Merge branch 'release' into feat/migrate_organization_to_workspaces

This commit is contained in:
Sidhant Goel 2022-05-18 12:52:23 +05:30
commit a3e593250d
No known key found for this signature in database
GPG Key ID: 0784E3B2D2D6C980
36 changed files with 615 additions and 213 deletions

View File

@ -0,0 +1,52 @@
const widgetsPage = require("../../../../locators/Widgets.json");
const dsl = require("../../../../fixtures/tableNewDsl.json");
const commonlocators = require("../../../../locators/commonlocators.json");
describe("Table Widget empty row color validation", function() {
before(() => {
cy.addDsl(dsl);
});
it("1. Validate cell background of columns", function() {
// Open property pane
cy.openPropertyPane("tablewidget");
// give general color to all table row
cy.selectColor("cellbackgroundcolor", -17);
cy.editColumn("id");
// Click on cell background color
cy.selectColor("cellbackground", -27);
cy.wait("@updateLayout");
cy.get(commonlocators.editPropBackButton).click({ force: true });
cy.editColumn("email");
cy.selectColor("cellbackground", -33);
cy.wait("@updateLayout");
cy.get(commonlocators.editPropBackButton).click({ force: true });
// Verify the cell background color of first column
cy.readTabledataValidateCSS(
"1",
"0",
"background-color",
"rgb(99, 102, 241)",
);
// Verify the cell background color of second column
cy.readTabledataValidateCSS(
"1",
"1",
"background-color",
"rgb(30, 58, 138)",
);
});
it("2. Validate empty row background", function() {
// first cell of first row should be transparent
cy.get(
".t--widget-tablewidget .tbody div[data-cy='empty-row-0-cell-0']",
).should("have.css", "background-color", "rgb(99, 102, 241)");
// second cell of first row should be transparent
cy.get(
".t--widget-tablewidget .tbody div[data-cy='empty-row-0-cell-1']",
).should("have.css", "background-color", "rgb(30, 58, 138)");
});
});

View File

@ -0,0 +1,228 @@
const datasource = require("../../../../locators/DatasourcesEditor.json");
const queryLocators = require("../../../../locators/QueryEditor.json");
const dynamicInputLocators = require("../../../../locators/DynamicInput.json");
const explorer = require("../../../../locators/explorerlocators.json");
describe("Git discard changes:", function() {
let datasourceName;
let repoName;
const query1 = "get_users";
const query2 = "get_allusers";
const jsObject = "JSObject1";
const page2 = "Page_2";
const page3 = "Page_3";
it("1. Create an app with Query1 and JSObject1, connect it to git", () => {
// Create new postgres datasource
cy.NavigateToDatasourceEditor();
cy.get(datasource.PostgreSQL).click();
cy.getPluginFormsAndCreateDatasource();
cy.fillPostgresDatasourceForm();
cy.testSaveDatasource();
cy.get("@createDatasource").then((httpResponse) => {
datasourceName = httpResponse.response.body.data.name;
cy.get(datasource.datasourceCard)
.contains(datasourceName)
.scrollIntoView()
.should("be.visible")
.closest(datasource.datasourceCard)
.within(() => {
cy.get(datasource.createQuerty).click();
});
});
// Create new postgres query
cy.get(queryLocators.queryNameField).type(`${query1}`);
cy.get(queryLocators.switch)
.last()
.click({ force: true });
cy.get(queryLocators.templateMenu).click();
cy.get(queryLocators.query).click({ force: true });
cy.get(".CodeMirror textarea")
.first()
.focus()
.type("SELECT * FROM users ORDER BY id LIMIT 10;", {
force: true,
parseSpecialCharSequences: false,
});
cy.WaitAutoSave();
cy.runQuery();
cy.CheckAndUnfoldEntityItem("PAGES");
cy.wait(1000);
cy.get(".t--entity-item:contains(Page1)")
.first()
.click();
cy.wait("@getPage");
// bind input widget to postgres query on page1
cy.get(explorer.addWidget).click();
cy.dragAndDropToCanvas("inputwidgetv2", { x: 300, y: 300 });
cy.get(".t--widget-inputwidgetv2").should("exist");
cy.get(dynamicInputLocators.input)
.eq(1)
.click({ force: true })
.type(`{{${query1}.data[0].name}}`, {
parseSpecialCharSequences: false,
});
cy.wait(2000);
cy.CheckAndUnfoldEntityItem("PAGES");
cy.Createpage(page2);
cy.wait(1000);
cy.get(`.t--entity-item:contains(${page2})`)
.first()
.click();
cy.wait("@getPage");
cy.createJSObject('return "Success";');
cy.get(explorer.addWidget).click();
// bind input widget to JSObject response on page2
cy.dragAndDropToCanvas("inputwidgetv2", { x: 300, y: 300 });
cy.get(".t--widget-inputwidgetv2").should("exist");
cy.get(dynamicInputLocators.input)
.eq(1)
.click({ force: true })
.type("{{JSObject1.myFun1()}}", { parseSpecialCharSequences: false });
cy.get("#switcher--explorer").click({ force: true });
// connect app to git
cy.generateUUID().then((uid) => {
repoName = uid;
cy.createTestGithubRepo(repoName);
cy.connectToGitRepo(repoName);
});
});
it("2. Add new datasource query, discard changes, verify query is deleted", () => {
cy.get(`.t--entity-item:contains("Page1")`)
.first()
.click();
cy.wait("@getPage");
// create new postgres query
cy.NavigateToQueryEditor();
cy.NavigateToActiveTab();
cy.get(datasource.datasourceCard)
.contains(datasourceName)
.scrollIntoView()
.should("be.visible")
.closest(datasource.datasourceCard)
.within(() => {
cy.get(datasource.createQuerty).click();
});
cy.get(queryLocators.queryNameField).type(`${query2}`);
cy.get(queryLocators.switch)
.last()
.click({ force: true });
cy.get(queryLocators.templateMenu).click();
cy.get(queryLocators.query).click({ force: true });
cy.get(".CodeMirror textarea")
.first()
.focus()
.type("SELECT * FROM users;", {
force: true,
parseSpecialCharSequences: false,
});
cy.WaitAutoSave();
cy.runQuery();
// navoigate to Page1
cy.get(`.t--entity-item:contains(Page1)`)
.first()
.click();
cy.wait("@getPage");
// discard changes
cy.gitDiscardChanges();
cy.CheckAndUnfoldEntityItem("QUERIES/JS");
// verify query2 is not present
cy.get(`.t--entity-name:contains(${query2})`).should("not.exist");
});
it("3. Add new JSObject , discard changes verify JSObject is deleted", () => {
cy.createJSObject('return "Success";');
cy.CheckAndUnfoldEntityItem("QUERIES/JS");
// verify jsObject is not duplicated
cy.get(`.t--entity-name:contains(${jsObject})`).should("have.length", 1);
cy.gitDiscardChanges();
cy.CheckAndUnfoldEntityItem("QUERIES/JS");
// verify jsObject2 is deleted after discarding changes
cy.get(`.t--entity-name:contains(${jsObject})`).should("not.exist");
});
it("4. Delete page2 and trigger discard flow, page2 should be available again", () => {
cy.Deletepage(page2);
// verify page is deleted
cy.CheckAndUnfoldEntityItem("PAGES");
cy.get(`.t--entity-name:contains(${page2})`).should("not.exist");
cy.gitDiscardChanges();
// verify page2 is recovered back
cy.get(`.t--entity-name:contains(${page2})`).should("be.visible");
cy.get(`.t--entity-item:contains(${page2})`)
.first()
.click();
cy.wait("@getPage");
// verify data binding on page2
cy.get(".bp3-input").should("have.value", "Success");
});
it("5. Delete Query1 and trigger discard flow, Query1 will be recovered", () => {
// navigate to Page1
cy.CheckAndUnfoldEntityItem("PAGES");
cy.get(`.t--entity-item:contains("Page1")`)
.first()
.click();
cy.wait("@getPage");
// delete query1
cy.deleteQueryOrJS(query1);
// verify Query1 is deleted
cy.get(`.t--entity-name:contains(${query1})`).should("not.exist");
// discard changes
cy.gitDiscardChanges();
//verify query1 is recovered
cy.get(`.t--entity-name:contains(${query1})`).should("be.visible");
cy.get(".bp3-input").should("have.value", "Test user 7");
});
it("6. Delete JSObject1 and trigger discard flow, JSObject1 should be active again", () => {
// navigate to page2
cy.CheckAndUnfoldEntityItem("PAGES");
cy.get(`.t--entity-item:contains(${page2})`)
.first()
.click();
cy.wait("@getPage");
// delete jsObject1
cy.CheckAndUnfoldEntityItem("QUERIES/JS");
cy.get(`.t--entity-item:contains(${jsObject})`).within(() => {
cy.get(".t--context-menu").click({ force: true });
});
cy.selectAction("Delete");
cy.selectAction("Are you sure?");
cy.get(`.t--entity-name:contains(${jsObject})`).should("not.exist");
// discard changes
cy.gitDiscardChanges();
//verify JSObject is recovered
cy.get(`.t--entity-name:contains(${jsObject})`).should("be.visible");
cy.get(".bp3-input").should("have.value", "Success");
});
it("7. Add new page i.e page3, go to page2 & discard changes, verify page3 is removed", () => {
// create new page page3 and move to page1
cy.Createpage(page3);
cy.get(`.t--entity-item:contains(${page2})`)
.first()
.click();
// discard changes
cy.gitDiscardChanges();
// verify page3 is removed
cy.CheckAndUnfoldEntityItem("PAGES");
cy.get(`.t--entity-name:contains("${page3}")`).should("not.exist");
});
it("8. Add new page i.e page3, discard changes should give error resource not found", () => {
cy.Createpage(page3);
cy.gitDiscardChanges(false);
cy.go("back");
cy.reload();
});
});

View File

@ -51,4 +51,5 @@ export default {
gitPullCount: ".t--bottom-bar-pull .count",
gitConnectionContainer: "[data-test=t--git-connection-container]",
gitRemoteURLContainer: "[data-test=t--remote-url-container]",
discardChanges: ".t--discard-button",
};

View File

@ -363,3 +363,37 @@ Cypress.Commands.add(
});
},
);
Cypress.Commands.add("gitDiscardChanges", (assertResourceFound = true) => {
cy.get(gitSyncLocators.bottomBarCommitButton).click();
//cy.intercept("GET", "/api/v1/git/status/*").as("gitStatus");
// cy.wait("@gitStatus").should(
// "have.nested.property",
// "response.body.responseMeta.status",
// 200,
// );
cy.get(gitSyncLocators.discardChanges)
.children()
.should("have.text", "Discard changes");
cy.get(gitSyncLocators.discardChanges).click();
cy.contains(Cypress.env("MESSAGES").DISCARD_CHANGES_WARNING());
cy.get(gitSyncLocators.discardChanges)
.children()
.should("have.text", "Are you sure?");
cy.get(gitSyncLocators.discardChanges).click();
cy.contains(Cypress.env("MESSAGES").DISCARDING_AND_PULLING_CHANGES());
if (assertResourceFound) {
cy.wait("@applications").should(
"have.nested.property",
"response.body.responseMeta.status",
200,
);
cy.validateToastMessage("Discarded changes successfully.");
} else {
cy.get(".bold-text").should(($x) => {
expect($x).contain("Page not found");
});
}
});

View File

@ -424,7 +424,7 @@ Cypress.Commands.add("updateCodeInput", ($selector, value) => {
});
});
Cypress.Commands.add("selectColor", (GivenProperty) => {
Cypress.Commands.add("selectColor", (GivenProperty, colorOffset = -15) => {
// Property pane of the widget is opened, and click given property.
cy.get(
".t--property-control-" + GivenProperty + " .bp3-input-group input",
@ -433,7 +433,7 @@ Cypress.Commands.add("selectColor", (GivenProperty) => {
});
cy.get(widgetsPage.colorPickerV2Color)
.eq(-15)
.eq(colorOffset)
.then(($elem) => {
cy.get($elem).click({ force: true });
});
@ -1109,7 +1109,16 @@ Cypress.Commands.add("clearPropertyValue", (value) => {
// eslint-disable-next-line cypress/no-unnecessary-waiting
cy.wait(1000);
});
Cypress.Commands.add("deleteQueryOrJS", (Action) => {
cy.CheckAndUnfoldEntityItem("QUERIES/JS");
cy.get(`.t--entity-item:contains(${Action})`).within(() => {
cy.get(".t--context-menu").click({ force: true });
});
cy.selectAction("Delete");
cy.selectAction("Are you sure?");
cy.wait("@deleteAction");
cy.get("@deleteAction").should("have.property", "status", 200);
});
Cypress.Commands.add(
"validateNSelectDropdown",
(ddTitle, currentValue, newValue) => {

View File

@ -83,6 +83,7 @@
"mammoth": "^1.4.19",
"marked": "^2.0.0",
"memoize-one": "^5.2.1",
"micro-memoize": "^4.0.10",
"moment": "^2.24.0",
"moment-timezone": "^0.5.27",
"nanoid": "^2.0.4",

View File

@ -55,7 +55,7 @@ export const StyledLink = styled(Link)<{ $active: boolean }>`
}
`;
export function useSettingsCategory() {
export function getSettingsCategory() {
return Array.from(AdminConfig.categories);
}
@ -109,7 +109,7 @@ export function Categories({
}
export default function LeftPane() {
const categories = useSettingsCategory();
const categories = getSettingsCategory();
const { category, subCategory } = useParams() as any;
return (
<Wrapper>

View File

@ -11,7 +11,7 @@ const Main = () => {
const wrapperCategory =
AdminConfig.wrapperCategories[subCategory ?? category];
if (!!wrapperCategory && !!wrapperCategory.component) {
if (!!wrapperCategory?.component) {
const { component: WrapperCategoryComponent } = wrapperCategory;
return <WrapperCategoryComponent />;
} else if (

View File

@ -25,7 +25,7 @@ const {
enableGoogleOAuth,
} = getAppsmithConfigs();
const Form_Auth: AdminConfigType = {
const FormAuth: AdminConfigType = {
type: SettingCategories.FORM_AUTH,
controlType: SettingTypes.GROUP,
title: "Form Login",
@ -65,7 +65,7 @@ const Form_Auth: AdminConfigType = {
],
};
const Google_Auth: AdminConfigType = {
const GoogleAuth: AdminConfigType = {
type: SettingCategories.GOOGLE_AUTH,
controlType: SettingTypes.GROUP,
title: "Google Authentication",
@ -111,7 +111,7 @@ const Google_Auth: AdminConfigType = {
],
};
const Github_Auth: AdminConfigType = {
const GithubAuth: AdminConfigType = {
type: SettingCategories.GITHUB_AUTH,
controlType: SettingTypes.GROUP,
title: "Github Authentication",
@ -149,7 +149,7 @@ const Github_Auth: AdminConfigType = {
],
};
export const Form_Auth_Callout: AuthMethodType = {
export const FormAuthCallout: AuthMethodType = {
id: "APPSMITH_FORM_LOGIN_AUTH",
category: SettingCategories.FORM_AUTH,
label: "Form Login",
@ -159,7 +159,7 @@ export const Form_Auth_Callout: AuthMethodType = {
isConnected: !disableLoginForm,
};
export const Google_Auth_Callout: AuthMethodType = {
export const GoogleAuthCallout: AuthMethodType = {
id: "APPSMITH_GOOGLE_AUTH",
category: SettingCategories.GOOGLE_AUTH,
label: "Google",
@ -170,7 +170,7 @@ export const Google_Auth_Callout: AuthMethodType = {
isConnected: enableGoogleOAuth,
};
export const Github_Auth_Callout: AuthMethodType = {
export const GithubAuthCallout: AuthMethodType = {
id: "APPSMITH_GITHUB_AUTH",
category: SettingCategories.GITHUB_AUTH,
label: "Github",
@ -181,7 +181,7 @@ export const Github_Auth_Callout: AuthMethodType = {
isConnected: enableGithubOAuth,
};
export const Saml_Auth_Callout: AuthMethodType = {
export const SamlAuthCallout: AuthMethodType = {
id: "APPSMITH_SAML_AUTH",
category: "saml",
label: "SAML 2.0",
@ -191,7 +191,7 @@ export const Saml_Auth_Callout: AuthMethodType = {
type: "OTHER",
};
export const Oidc_Auth_Callout: AuthMethodType = {
export const OidcAuthCallout: AuthMethodType = {
id: "APPSMITH_OIDC_AUTH",
category: "oidc",
label: "OIDC",
@ -202,11 +202,11 @@ export const Oidc_Auth_Callout: AuthMethodType = {
};
const AuthMethods = [
Oidc_Auth_Callout,
Saml_Auth_Callout,
Google_Auth_Callout,
Github_Auth_Callout,
Form_Auth_Callout,
OidcAuthCallout,
SamlAuthCallout,
GoogleAuthCallout,
GithubAuthCallout,
FormAuthCallout,
];
function AuthMain() {
@ -218,6 +218,6 @@ export const config: AdminConfigType = {
controlType: SettingTypes.PAGE,
title: "Authentication",
canSave: false,
children: [Form_Auth, Google_Auth, Github_Auth],
children: [FormAuth, GoogleAuth, GithubAuth],
component: AuthMain,
};

View File

@ -7,7 +7,8 @@ type DropdownWrapperProps = {
value?: string;
onChange?: (value?: string) => void;
};
options: Array<{ id: string; value: string; label: string }>;
options: Array<{ id: string; value: string; label?: string }>;
fillOptions?: boolean;
};
function DropdownWrapper(props: DropdownWrapperProps) {
@ -28,6 +29,7 @@ function DropdownWrapper(props: DropdownWrapperProps) {
return (
<Dropdown
fillOptions={props.fillOptions}
onSelect={onSelectHandler}
options={props.options}
selected={selectedOption}

View File

@ -18,15 +18,17 @@ const renderComponent = (
type SelectFieldProps = {
name: string;
placeholder: string;
options: Array<{ id: string; value: string; label: string }>;
options: Array<{ id: string; value: string; label?: string }>;
size?: "large" | "small";
outline?: boolean;
fillOptions?: boolean;
};
export function SelectField(props: SelectFieldProps) {
return (
<Field
component={renderComponent}
fillOptions={props.fillOptions}
name={props.name}
options={props.options}
outline={props.outline}

View File

@ -23,6 +23,7 @@ import {
} from "entities/DataTree/actionTriggers";
import { AppTheme } from "entities/AppTheming";
import { PluginId } from "api/PluginApi";
import log from "loglevel";
export type ActionDispatcher = (
...args: any[]
@ -164,6 +165,9 @@ export class DataTreeFactory {
widgetsMeta,
}: DataTreeSeed): DataTree {
const dataTree: DataTree = {};
const start = performance.now();
const startActions = performance.now();
actions.forEach((action) => {
const editorConfig = editorConfigs[action.config.pluginId];
const dependencyConfig = pluginDependencyConfig[action.config.pluginId];
@ -173,15 +177,24 @@ export class DataTreeFactory {
dependencyConfig,
);
});
const endActions = performance.now();
const startJsActions = performance.now();
jsActions.forEach((js) => {
dataTree[js.config.name] = generateDataTreeJSAction(js);
});
const endJsActions = performance.now();
const startWidgets = performance.now();
Object.values(widgets).forEach((widget) => {
dataTree[widget.widgetName] = generateDataTreeWidget(
widget,
widgetsMeta[widget.widgetId],
);
});
const endWidgets = performance.now();
dataTree.pageList = pageList;
dataTree.appsmith = {
@ -192,6 +205,17 @@ export class DataTreeFactory {
theme,
} as DataTreeAppsmith;
(dataTree.appsmith as DataTreeAppsmith).ENTITY_TYPE = ENTITY_TYPE.APPSMITH;
const end = performance.now();
const out = {
total: end - start,
widgets: endWidgets - startWidgets,
actions: endActions - startActions,
jsActions: endJsActions - startJsActions,
};
log.debug("### Create unevalTree timing", out);
return dataTree;
}
}

View File

@ -289,7 +289,6 @@ describe("generateDataTreeWidget", () => {
privateWidgets: {},
deepObj: {
level1: {
value: 10,
metaValue: 10,
},
},

View File

@ -1,22 +1,28 @@
import WidgetFactory from "utils/WidgetFactory";
import { getAllPathsFromPropertyConfig } from "entities/Widget/utils";
import { getEntityDynamicBindingPathList } from "utils/DynamicBindingUtils";
import _ from "lodash";
import memoize from "micro-memoize";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { setOverridingProperty } from "./utils";
import { getEntityDynamicBindingPathList } from "utils/DynamicBindingUtils";
import WidgetFactory from "utils/WidgetFactory";
import {
OverridingPropertyPaths,
PropertyOverrideDependency,
OverridingPropertyType,
DataTreeWidget,
ENTITY_TYPE,
OverridingPropertyPaths,
OverridingPropertyType,
PropertyOverrideDependency,
} from "./dataTreeFactory";
import { setOverridingProperty } from "./utils";
export const generateDataTreeWidget = (
// We are splitting generateDataTreeWidget into two parts to memoize better as the widget doesn't change very often.
// Widget changes only when dynamicBindingPathList changes.
// Only meta properties change very often, for example typing in an input or selecting a table row.
const generateDataTreeWidgetWithoutMeta = (
widget: FlattenedWidgetProps,
widgetMetaProps: Record<string, unknown> = {},
): DataTreeWidget => {
): {
dataTreeWidgetWithoutMetaProps: DataTreeWidget;
overridingMetaPropsMap: Record<string, boolean>;
defaultMetaProps: Record<string, unknown>;
} => {
const derivedProps: any = {};
const blockedDerivedProps: Record<string, true> = {};
const unInitializedDefaultProps: Record<string, undefined> = {};
@ -94,16 +100,6 @@ export const generateDataTreeWidget = (
},
);
const overridingMetaProps: Record<string, unknown> = {};
// overridingMetaProps has all meta property value either from metaReducer or default set by widget whose dependent property also has default property.
Object.entries(defaultMetaProps).forEach(([key, value]) => {
if (overridingMetaPropsMap[key]) {
overridingMetaProps[key] =
key in widgetMetaProps ? widgetMetaProps[key] : value;
}
});
const {
bindingPaths,
reactivePaths,
@ -134,12 +130,12 @@ export const generateDataTreeWidget = (
*
* Therefore spread is replaced with "merge" which merges objects recursively.
*/
return _.merge(
const dataTreeWidgetWithoutMetaProps = _.merge(
{},
widget,
unInitializedDefaultProps,
defaultMetaProps,
widgetMetaProps,
// widgetMetaProps,
derivedProps,
{
defaultProps,
@ -149,7 +145,7 @@ export const generateDataTreeWidget = (
...widget.logBlackList,
...blockedDerivedProps,
},
meta: _.merge(overridingMetaProps, widgetMetaProps),
// meta: _.merge(overridingMetaProps, widgetMetaProps),
propertyOverrideDependency,
overridingPropertyPaths,
bindingPaths,
@ -162,4 +158,59 @@ export const generateDataTreeWidget = (
},
},
);
return {
dataTreeWidgetWithoutMetaProps,
overridingMetaPropsMap,
defaultMetaProps,
};
};
// @todo set the max size dynamically based on number of widgets. (widgets.length)
// Remove the debug statements in July 2022
const generateDataTreeWidgetWithoutMetaMemoized = memoize(
generateDataTreeWidgetWithoutMeta,
{
maxSize: 1000,
// onCacheHit: (cache, options) => {
// console.log("####### cache was hit: ", cache.keys.length);
// },
// onCacheAdd: (cache, options) => {
// console.log(
// "####### cache was missed ",
// cache.keys.length,
// cache.keys[0][0].widgetName,
// );
// },
},
);
export const generateDataTreeWidget = (
widget: FlattenedWidgetProps,
widgetMetaProps: Record<string, unknown> = {},
) => {
const {
dataTreeWidgetWithoutMetaProps: dataTreeWidget,
defaultMetaProps,
overridingMetaPropsMap,
} = generateDataTreeWidgetWithoutMetaMemoized(widget);
const overridingMetaProps: Record<string, unknown> = {};
// overridingMetaProps has all meta property value either from metaReducer or default set by widget whose dependent property also has default property.
Object.entries(defaultMetaProps).forEach(([key, value]) => {
if (overridingMetaPropsMap[key]) {
overridingMetaProps[key] =
key in widgetMetaProps ? widgetMetaProps[key] : value;
}
});
const meta = _.merge(overridingMetaProps, widgetMetaProps);
Object.entries(widgetMetaProps).forEach(([key, value]) => {
// Since meta properties are always updated as a whole, we are replacing instead of merging.
// Merging mutates the memoized value, avoid merging meta values
dataTreeWidget[key] = value;
});
dataTreeWidget["meta"] = meta;
return dataTreeWidget;
};

View File

@ -1,11 +1,12 @@
import { WidgetProps } from "widgets/BaseWidget";
import {
PropertyPaneConfig,
ValidationConfig,
} from "constants/PropertyControlConstants";
import { get, isObject, isUndefined, omitBy } from "lodash";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import { get, isObject, isUndefined, omitBy } from "lodash";
import memoize from "micro-memoize";
import { FlattenedWidgetProps } from "reducers/entityReducers/canvasWidgetsReducer";
import { WidgetProps } from "widgets/BaseWidget";
/**
* @typedef {Object} Paths
@ -163,7 +164,7 @@ const childHasPanelConfig = (
return { reactivePaths, triggerPaths, validationPaths, bindingPaths };
};
export const getAllPathsFromPropertyConfig = (
const getAllPathsFromPropertyConfigWithoutMemo = (
widget: WidgetProps,
widgetConfig: readonly PropertyPaneConfig[],
defaultProperties: Record<string, any>,
@ -276,6 +277,11 @@ export const getAllPathsFromPropertyConfig = (
return { reactivePaths, triggerPaths, validationPaths, bindingPaths };
};
export const getAllPathsFromPropertyConfig = memoize(
getAllPathsFromPropertyConfigWithoutMemo,
{ maxSize: 1000 },
);
/**
* this function gets the next available row for pasting widgets
* NOTE: this function excludes modal widget when calculating next available row

View File

@ -357,9 +357,9 @@ function OrgMenuItem({ isFetchingApplications, org, selected }: any) {
isFetchingApplications ? BlueprintClasses.SKELETON : ""
}
ellipsize={20}
href={`${window.location.pathname}#${org.organization.slug}`}
href={`${window.location.pathname}#${org.organization.id}`}
icon="workspace"
key={org.organization.slug}
key={org.organization.id}
ref={menuRef}
selected={selected}
text={org.organization.name}
@ -419,9 +419,9 @@ function LeftPane() {
userOrgs.map((org: any) => (
<OrgMenuItem
isFetchingApplications={isFetchingApplications}
key={org.organization.slug}
key={org.organization.id}
org={org}
selected={urlHash === org.organization.slug}
selected={urlHash === org.organization.id}
/>
))}
</WorkpsacesNavigator>
@ -652,7 +652,7 @@ function ApplicationsSection(props: any) {
{(currentUser || isFetchingApplications) &&
OrgMenuTarget({
orgName: organization.name,
orgSlug: organization.slug,
orgSlug: organization.id,
})}
{hasManageOrgPermissions && (
<Dialog
@ -739,7 +739,7 @@ function ApplicationsSection(props: any) {
closeOnItemClick
cypressSelector="t--org-name"
disabled={isFetchingApplications}
isOpen={organization.slug === orgToOpenMenu}
isOpen={organization.id === orgToOpenMenu}
onClose={() => {
setOrgToOpenMenu(null);
}}
@ -753,7 +753,7 @@ function ApplicationsSection(props: any) {
className="t--options-icon"
name="context-menu"
onClick={() => {
setOrgToOpenMenu(organization.slug);
setOrgToOpenMenu(organization.id);
}}
size={IconSize.XXXL}
/>

View File

@ -15,6 +15,14 @@ const Wrapper = styled(EntityTogglesWrapper)`
}
}
}
&.selected {
background: ${Colors.SHARK2} !important;
svg {
path {
fill: ${Colors.WHITE} !important;
}
}
}
`;
const PlusIcon = ControlIcons.INCREASE_CONTROL_V2;

View File

@ -236,7 +236,10 @@ export default function ExplorerSubMenu({
hoverOpenDelay={TOOLTIP_HOVER_ON_DELAY}
position={Position.RIGHT}
>
<EntityAddButton className={className} onClick={() => setShow(true)} />
<EntityAddButton
className={`${className} ${show ? "selected" : ""}`}
onClick={() => setShow(true)}
/>
</TooltipComponent>
</Popover2>
);

View File

@ -1,6 +1,6 @@
import React from "react";
import { FormGroup, SettingComponentProps } from "./Common";
import SelectField from "components/ads/formFields/SelectField";
import SelectField from "components/editorComponents/form/fields/SelectField";
export default function DropDown(
props: {

View File

@ -59,7 +59,7 @@ function getSettingDetail(category: string, subCategory: string) {
return AdminConfig.getCategoryDetails(category, subCategory);
}
function useSettings(category: string, subCategory?: string) {
function getSettingsConfig(category: string, subCategory?: string) {
return AdminConfig.get(subCategory ?? category);
}
@ -68,7 +68,7 @@ export function SettingsForm(
) {
const params = useParams() as any;
const { category, subCategory } = params;
const settingsDetails = useSettings(category, subCategory);
const settingsDetails = getSettingsConfig(category, subCategory);
const { settings, settingsConfig } = props;
const details = getSettingDetail(category, subCategory);
const dispatch = useDispatch();

View File

@ -8,8 +8,8 @@ export const Wrapper = styled.div`
overflow: auto;
`;
export const HeaderWrapper = styled.div`
margin-bottom: 16px;
export const HeaderWrapper = styled.div<{ margin?: string }>`
margin-bottom: ${(props) => props.margin ?? `16px`};
`;
export const SettingsHeader = styled.h2`

View File

@ -258,8 +258,8 @@ export function* createOrgSaga(
}
// get created org in focus
const slug = response.data.slug;
history.push(`${window.location.pathname}#${slug}`);
const orgId = response.data.id;
history.push(`${window.location.pathname}#${orgId}`);
} catch (error) {
yield call(reject, { _error: error.message });
yield put({

View File

@ -164,7 +164,6 @@ function MultiSelectComponent({
}, []);
const id = _.uniqueId();
console.log("dropDownWidth", dropDownWidth);
return (
<MultiSelectContainer
className={loading ? Classes.SKELETON : ""}

View File

@ -24,7 +24,7 @@ import {
TableStyles,
MenuItems,
} from "./Constants";
import { isString, isEmpty, findIndex, isNil, isNaN } from "lodash";
import { isString, isEmpty, findIndex, isNil, isNaN, get, set } from "lodash";
import PopoverVideo from "widgets/VideoWidget/component/PopoverVideo";
import AutoToolTipComponent from "widgets/TableWidget/component/AutoToolTipComponent";
import { ControlIcons } from "icons/ControlIcons";
@ -532,11 +532,19 @@ export const renderEmptyRows = (
renderCheckBoxCell(false, accentColor, borderRadius)}
{row.cells.map((cell: any, cellIndex: number) => {
const cellProps = cell.getCellProps();
if (columns[0]?.columnProperties?.cellBackground) {
cellProps.style.background =
columns[0].columnProperties.cellBackground;
}
return <div {...cellProps} className="td" key={cellIndex} />;
set(
cellProps,
"style.backgroundColor",
get(cell, "column.columnProperties.cellBackground"),
);
return (
<div
{...cellProps}
className="td"
data-cy={`empty-row-${index}-cell-${cellIndex}`}
key={cellIndex}
/>
);
})}
</div>
);
@ -568,6 +576,10 @@ export const renderEmptyRows = (
width: column.width + "px",
boxSizing: "border-box",
flex: `${column.width} 0 auto`,
backgroundColor: get(
column,
"columnProperties.cellBackground",
),
}}
/>
);

View File

@ -11464,6 +11464,11 @@ methods@~1.1.2:
version "1.1.2"
resolved "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz"
micro-memoize@^4.0.10:
version "4.0.10"
resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.0.10.tgz#cedf7682df990cd2290700af4537afa6dba7d4e9"
integrity sha512-rk0OlvEQkShjbr2EvGn1+GdCsgLDgABQyM9ZV6VoHNU7hiNM+eSOkjGWhiNabU/XWiEalWbjNQrNO+zcqd+pEA==
microevent.ts@~0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0"

View File

@ -43,14 +43,6 @@ public class Organization extends BaseDomain {
@JsonIgnore
private String logoAssetId;
public String makeSlug() {
return toSlug(name);
}
public static String toSlug(String text) {
return text == null ? null : text.replaceAll("[^\\w\\d]+", "-").toLowerCase();
}
public String getLogoUrl() {
return Url.ASSET_URL + "/" + logoAssetId;
}

View File

@ -69,7 +69,6 @@ import com.appsmith.server.dtos.WorkspacePluginStatus;
import com.appsmith.server.dtos.PageDTO;
import com.appsmith.server.helpers.GitDeployKeyGenerator;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.services.WorkspaceService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.cloudyrock.mongock.ChangeLog;
import com.github.cloudyrock.mongock.ChangeSet;
@ -311,23 +310,11 @@ public class DatabaseChangelog {
dropIndexIfExists(mongoTemplate, Organization.class, "name");
}
@ChangeSet(order = "003", id = "add-org-slugs", author = "")
public void addOrgSlugs(MongockTemplate mongoTemplate, WorkspaceService workspaceService) {
// For all existing organizations, add a slug field, which should be unique.
// We are blocking here for adding a slug to each existing organization. This is bad and slow. Do NOT copy this
// code fragment into the services' control flow. This is a single migration code and is expected to run once in
// lifetime of a deployment.
for (Organization organization : mongoTemplate.findAll(Organization.class)) {
if (organization.getSlug() == null) {
workspaceService.getNextUniqueSlug(organization.makeSlug())
.doOnSuccess(slug -> {
organization.setSlug(slug);
mongoTemplate.save(organization);
})
.block();
}
}
}
/*
* @ChangeSet(order = "003", id = "add-org-slugs", author = "")
* This migration has been removed as it's no more required. A newer version of this migration has been added
* in the @ChangeLog(order = "008") with id="update-organization-slugs"
*/
/**
* We are creating indexes manually because Spring's index resolver creates indexes on fields as well.
@ -368,11 +355,6 @@ public class DatabaseChangelog {
makeIndex("email").unique()
);
ensureIndexes(mongoTemplate, Organization.class,
createdAtIndex,
makeIndex("slug").unique()
);
ensureIndexes(mongoTemplate, Page.class,
createdAtIndex,
makeIndex("applicationId", "name").unique().named("application_page_compound_index")

View File

@ -15,12 +15,14 @@ import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.QApplication;
import com.appsmith.server.domains.QNewAction;
import com.appsmith.server.domains.QNewPage;
import com.appsmith.server.domains.QOrganization;
import com.appsmith.server.domains.QPlugin;
import com.appsmith.server.domains.Sequence;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.TextUtils;
import com.github.cloudyrock.mongock.ChangeLog;
import com.github.cloudyrock.mongock.ChangeSet;
import com.github.cloudyrock.mongock.driver.mongodb.springdata.v3.decorator.impl.MongockTemplate;
@ -751,7 +753,32 @@ public class DatabaseChangelog2 {
);
}
@ChangeSet(order = "008", id = "copy-organization-to-workspaces", author = "")
/**
* We'll remove the uniqe index on organization slugs. We'll also regenerate the slugs for all organizations as
* most of them are outdated
* @param mongockTemplate MongockTemplate instance
*/
@ChangeSet(order = "008", id = "update-organization-slugs", author = "")
public void updateOrganizationSlugs(MongockTemplate mongockTemplate) {
dropIndexIfExists(mongockTemplate, Organization.class, "slug");
// update organizations
final Query getAllOrganizationsQuery = query(where("deletedAt").is(null));
getAllOrganizationsQuery.fields()
.include(fieldName(QOrganization.organization.name));
List<Organization> organizations = mongockTemplate.find(getAllOrganizationsQuery, Organization.class);
for (Organization organization : organizations) {
mongockTemplate.updateFirst(
query(where(fieldName(QOrganization.organization.id)).is(organization.getId())),
new Update().set(fieldName(QOrganization.organization.slug), TextUtils.makeSlug(organization.getName())),
Organization.class
);
}
}
@ChangeSet(order = "009", id = "copy-organization-to-workspaces", author = "")
public void copyOrganizationToWorkspaces(MongockTemplate mongockTemplate) {
Gson gson = new Gson();
for (Organization organization : mongockTemplate.findAll(Organization.class)) {
@ -760,21 +787,19 @@ public class DatabaseChangelog2 {
}
}
/**
* We are creating indexes manually because Spring's index resolver creates indexes on fields as well.
* See https://stackoverflow.com/questions/60867491/ for an explanation of the problem. We have that problem with
* the `Action.datasource` field.
*/
@ChangeSet(order = "009", id = "add-workspace-indexes", author = "")
@ChangeSet(order = "010", id = "add-workspace-indexes", author = "")
public void addWorkspaceIndexes(MongockTemplate mongockTemplate) {
ensureIndexes(mongockTemplate, Workspace.class,
makeIndex("createdAt"),
makeIndex("slug").unique()
makeIndex("createdAt")
);
}
@ChangeSet(order = "010", id = "update-sequence-names-from-organization-to-workspace", author = "")
@ChangeSet(order = "011", id = "update-sequence-names-from-organization-to-workspace", author = "")
public void updateSequenceNamesFromOrganizationToWorkspace(MongockTemplate mongockTemplate) {
for (Sequence sequence : mongockTemplate.findAll(Sequence.class)) {
String oldName = sequence.getName();

View File

@ -15,8 +15,6 @@ public interface CustomWorkspaceRepositoryCE extends AppsmithRepository<Workspac
Flux<Workspace> findByIdsIn(Set<String> orgIds, AclPermission aclPermission, Sort sort);
Mono<Long> nextSlugNumber(String slugPrefix);
Mono<Void> updateUserRoleNames(String userId, String userName);
Flux<Workspace> findAllWorkspaces();

View File

@ -16,10 +16,8 @@ import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static org.springframework.data.mongodb.core.query.Criteria.where;
import static org.springframework.data.mongodb.core.query.Query.query;
@Slf4j
public class CustomWorkspaceRepositoryCEImpl extends BaseAppsmithRepositoryImpl<Workspace>
@ -43,29 +41,6 @@ public class CustomWorkspaceRepositoryCEImpl extends BaseAppsmithRepositoryImpl<
return queryAll(List.of(orgIdsCriteria), aclPermission, sort);
}
@Override
public Mono<Long> nextSlugNumber(String slugPrefix) {
final String slugField = fieldName(QWorkspace.workspace.slug);
final Query slugPrefixQuery = query(where(slugField).regex("^" + slugPrefix + "\\d*$"));
slugPrefixQuery.fields().include(slugField);
return mongoOperations
.find(slugPrefixQuery, Workspace.class)
.map(Workspace::getSlug)
.collect(Collectors.toSet())
.map(slugs -> {
if (slugs.isEmpty() || !slugs.contains(slugPrefix)) {
return 0L;
}
long number = 1L;
while (slugs.contains(slugPrefix + number)) {
++number;
}
return number;
});
}
@Override
public Mono<Void> updateUserRoleNames(String userId, String userName) {
return mongoOperations

View File

@ -909,7 +909,7 @@ public class UserServiceCEImpl extends BaseService<UserRepository, User, String>
if (isNewUser) {
params.put("inviteUrl", inviteUrl);
} else {
params.put("inviteUrl", inviteUrl + "/applications#" + workspace.getSlug());
params.put("inviteUrl", inviteUrl + "/applications#" + workspace.getId());
}
return params;
}

View File

@ -17,10 +17,6 @@ public interface WorkspaceServiceCE extends CrudService<Workspace, String> {
Mono<Workspace> create(Workspace workspace);
Mono<Workspace> getBySlug(String slug);
Mono<String> getNextUniqueSlug(String initialSlug);
Mono<Workspace> createDefault(Workspace workspace, User user);
Mono<Workspace> create(Workspace workspace, User user);

View File

@ -14,6 +14,7 @@ import com.appsmith.server.domains.UserRole;
import com.appsmith.server.dtos.WorkspacePluginStatus;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.AssetRepository;
import com.appsmith.server.repositories.WorkspaceRepository;
@ -103,17 +104,6 @@ public class WorkspaceServiceCEImpl extends BaseService<WorkspaceRepository, Wor
});
}
@Override
public Mono<Workspace> getBySlug(String slug) {
return repository.findBySlug(slug);
}
@Override
public Mono<String> getNextUniqueSlug(String initialSlug) {
return repository.nextSlugNumber(initialSlug)
.map(number -> initialSlug + (number == 0 ? "" : number));
}
/**
* Creates the given workspace as a default workspace for the given user. That is, the workspace's name
* is changed to "[username]'s apps" and then created. The current value of the workspace name
@ -160,19 +150,9 @@ public class WorkspaceServiceCEImpl extends BaseService<WorkspaceRepository, Wor
workspace.setEmail(user.getEmail());
}
Mono<Workspace> setSlugMono;
if (workspace.getName() == null) {
setSlugMono = Mono.just(workspace);
} else {
setSlugMono = getNextUniqueSlug(workspace.makeSlug())
.map(slug -> {
workspace.setSlug(slug);
return workspace;
});
}
workspace.setSlug(TextUtils.makeSlug(workspace.getName()));
return setSlugMono
.flatMap(this::validateObject)
return validateObject(workspace)
// Install all the default plugins when the org is created
/* TODO: This is a hack. We should ideally use the pluginService.installPlugin() function.
Not using it right now because of circular dependency b/w workspaceService and pluginService
@ -226,6 +206,10 @@ public class WorkspaceServiceCEImpl extends BaseService<WorkspaceRepository, Wor
resource.setPolicies(null);
}
if(StringUtils.hasLength(resource.getName())) {
resource.setSlug(TextUtils.makeSlug(resource.getName()));
}
return findWorkspaceMono
.map(existingWorkspace -> {
AppsmithBeanUtils.copyNewFieldValuesIntoOldObject(resource, existingWorkspace);
@ -248,12 +232,15 @@ public class WorkspaceServiceCEImpl extends BaseService<WorkspaceRepository, Wor
@Override
public Mono<Workspace> save(Workspace workspace) {
if(StringUtils.hasLength(workspace.getName())) {
workspace.setSlug(TextUtils.makeSlug(workspace.getName()));
}
return repository.save(workspace);
}
@Override
public Mono<Workspace> findByIdAndPluginsPluginId(String organizationId, String pluginId) {
return repository.findByIdAndPluginsPluginId(organizationId, pluginId);
public Mono<Workspace> findByIdAndPluginsPluginId(String workspaceId, String pluginId) {
return repository.findByIdAndPluginsPluginId(workspaceId, pluginId);
}
@Override

View File

@ -51,6 +51,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_USERS;
@ -94,15 +95,12 @@ public class UserServiceTest {
Mono<User> userMono;
Mono<Workspace> workspaceMono;
@Autowired
UserSignup userSignup;
@Before
public void setup() {
userMono = userService.findByEmail("usertest@usertest.com");
workspaceMono = workspaceService.getBySlug("spring-test-workspace");
}
//Test if email params are updating correctly
@ -110,13 +108,13 @@ public class UserServiceTest {
public void checkEmailParamsForExistingUser() {
Workspace workspace = new Workspace();
workspace.setName("UserServiceTest Update Org");
workspace.setSlug("userservicetest-update-org");
workspace.setId(UUID.randomUUID().toString());
User inviter = new User();
inviter.setName("inviterUserToApplication");
String inviteUrl = "http://localhost:8080";
String expectedUrl = inviteUrl + "/applications#userservicetest-update-org";
String expectedUrl = inviteUrl + "/applications#" + workspace.getId();
Map<String, String> params = userService.getEmailParams(workspace, inviter, inviteUrl, false);
assertEquals(expectedUrl, params.get("inviteUrl"));
@ -127,8 +125,8 @@ public class UserServiceTest {
@Test
public void checkEmailParamsForNewUser() {
Workspace workspace = new Workspace();
workspace.setId(UUID.randomUUID().toString());
workspace.setName("UserServiceTest Update Org");
workspace.setSlug("userservicetest-update-org");
User inviter = new User();
inviter.setName("inviterUserToApplication");

View File

@ -2,24 +2,20 @@ package com.appsmith.server.services;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AppsmithRole;
import com.appsmith.server.configurations.CommonConfig;
import com.appsmith.server.configurations.WithMockAppsmithUser;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.InviteUser;
import com.appsmith.server.domains.LoginSource;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.domains.User;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.repositories.UserRepository;
import com.appsmith.server.solutions.UserSignup;
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.test.context.support.WithUserDetails;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner;
@ -42,32 +38,17 @@ public class UserServiceWithDisabledSignupTest {
@Autowired
UserService userService;
@Autowired
WorkspaceService workspaceService;
@Autowired
ApplicationService applicationService;
@Autowired
UserRepository userRepository;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
CommonConfig commonConfig;
Mono<User> userMono;
Mono<Workspace> workspaceMono;
@Autowired
UserSignup userSignup;
@Before
public void setup() {
userMono = userService.findByEmail("usertest@usertest.com");
workspaceMono = workspaceService.getBySlug("spring-test-workspace");
}
@Test
@ -232,8 +213,8 @@ public class UserServiceWithDisabledSignupTest {
StepVerifier.create(userMono)
.assertNext(user -> {
assertThat(user.getEmail().equals(newUser.getEmail()));
assertThat(user.getSource().equals(LoginSource.FORM));
assertThat(user.getEmail()).isEqualTo(newUser.getEmail());
assertThat(user.getSource()).isEqualTo(LoginSource.FORM);
assertThat(user.getIsEnabled()).isTrue();
})
.verifyComplete();

View File

@ -16,6 +16,7 @@ import com.appsmith.server.domains.UserRole;
import com.appsmith.server.dtos.InviteUsersDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.repositories.AssetRepository;
import com.appsmith.server.repositories.DatasourceRepository;
import com.appsmith.server.repositories.WorkspaceRepository;
@ -49,6 +50,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES;
@ -196,7 +198,7 @@ public class WorkspaceServiceTest {
assertThat(workspace1.getName()).isEqualTo("Test Name");
assertThat(workspace1.getPolicies()).isNotEmpty();
assertThat(workspace1.getPolicies()).containsAll(Set.of(manageOrgAppPolicy, manageOrgPolicy));
assertThat(workspace1.getSlug() != null);
assertThat(workspace1.getSlug()).isEqualTo(TextUtils.makeSlug(workspace.getName()));
assertThat(workspace1.getEmail()).isEqualTo("api_user");
assertThat(workspace1.getIsAutoGeneratedOrganization()).isNull();
})
@ -313,24 +315,6 @@ public class WorkspaceServiceTest {
.verify();
}
@Test
@WithUserDetails(value = "api_user")
public void uniqueSlugs() {
Workspace workspace = new Workspace();
workspace.setName("Slug org");
workspace.setDomain("example.com");
workspace.setWebsite("https://example.com");
Mono<String> uniqueSlug = workspaceService.create(workspace)
.flatMap(org -> workspaceService.getNextUniqueSlug(org.getSlug()));
StepVerifier.create(uniqueSlug)
.assertNext(slug -> {
assertThat(slug).isEqualTo("slug-org1");
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void createDuplicateNameWorkspace() {
@ -349,8 +333,11 @@ public class WorkspaceServiceTest {
StepVerifier.create(Mono.zip(firstOrgCreation, secondOrgCreation))
.assertNext(orgsTuple -> {
assertThat(orgsTuple.getT1().getName()).isEqualTo("Really good org");
assertThat(orgsTuple.getT1().getSlug()).isEqualTo("really-good-org");
assertThat(orgsTuple.getT2().getSlug()).isEqualTo("really-good-org1");
assertThat(orgsTuple.getT2().getName()).isEqualTo("Really good org");
assertThat(orgsTuple.getT2().getSlug()).isEqualTo("really-good-org");
})
.verifyComplete();
}
@ -1183,4 +1170,49 @@ public class WorkspaceServiceTest {
.create(deleteOrgMono)
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void save_WhenNameIsPresent_SlugGenerated() {
String uniqueString = UUID.randomUUID().toString(); // to make sure name is not conflicted with other tests
Workspace workspace = new Workspace();
workspace.setName("My Organization " + uniqueString);
String finalName = "Renamed Organization " + uniqueString;
Mono<Workspace> workspaceMono = workspaceService.create(workspace)
.flatMap(savedWorkspace -> {
savedWorkspace.setName(finalName);
return workspaceService.save(savedWorkspace);
});
StepVerifier.create(workspaceMono)
.assertNext(savedWorkspace -> {
assertThat(savedWorkspace.getSlug()).isEqualTo(TextUtils.makeSlug(finalName));
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void update_WhenNameIsNotPresent_SlugIsNotGenerated() {
String uniqueString = UUID.randomUUID().toString(); // to make sure name is not conflicted with other tests
String initialName = "My Organization " + uniqueString;
Workspace workspace = new Workspace();
workspace.setName(initialName);
Mono<Workspace> workspaceMono = workspaceService.create(workspace)
.flatMap(savedWorkspace -> {
Workspace workspaceDto = new Workspace();
workspaceDto.setWebsite("https://appsmith.com");
return workspaceService.update(savedWorkspace.getId(), workspaceDto);
});
StepVerifier.create(workspaceMono)
.assertNext(savedWorkspace -> {
// slug should be unchanged
assertThat(savedWorkspace.getSlug()).isEqualTo(TextUtils.makeSlug(initialName));
})
.verifyComplete();
}
}