Merge branch 'release' into feat/4182-form-detect-changes

This commit is contained in:
Paul Li 2022-02-23 16:21:48 +08:00
commit ff2bcc228a
63 changed files with 2639 additions and 733 deletions

View File

@ -61,7 +61,7 @@
"options": "[{'label':'Vegetarian','value':'VEG'},{'label':'Non-Vegetarian','value':'NON_VEG'},{'label':'Vegan','value':'VEGAN'}]",
"widgetName": "Dropdown1",
"defaultOptionValue": "VEG",
"type": "DROP_DOWN_WIDGET",
"type": "SELECT_WIDGET",
"isLoading": false,
"parentColumnSpace": 74,
"parentRowSpace": 40,

View File

@ -127,7 +127,7 @@
"topRow": 8,
"bottomRow": 15,
"parentRowSpace": 10,
"type": "DROP_DOWN_WIDGET",
"type": "SELECT_WIDGET",
"serverSideFiltering": false,
"hideCard": false,
"defaultOptionValue": "GREEN",

View File

@ -65,7 +65,7 @@
"parentRowSpace": 38,
"isVisible": true,
"label": "Test Dropdown",
"type": "DROP_DOWN_WIDGET",
"type": "SELECT_WIDGET",
"dynamicBindingPathList": [],
"isLoading": false,
"selectionType": "",

View File

@ -25,6 +25,7 @@ describe("API Panel request body", function() {
cy.SelectAction(testdata.getAction);
cy.contains(apiEditor.bodyTab).click();
cy.get(`[data-cy=${testdata.apiContentTypeNone}]`).click();
cy.get(testdata.noBodyErrorMessageDiv).should("exist");
cy.get(testdata.noBodyErrorMessageDiv).contains(
testdata.noBodyErrorMessage,

View File

@ -14,13 +14,11 @@ describe("Check debugger logs state when there are onPageLoad actions", function
cy.CreateAPI("TestApi");
cy.enterDatasourceAndPath(testdata.baseUrl, testdata.methods);
cy.SaveAndRunAPI();
cy.get(explorer.addWidget).click();
cy.reload();
// Wait for the debugger icon to be visible
cy.get(".t--debugger").should("be.visible");
cy.get(debuggerLocators.errorCount).should("not.exist");
//cy.get(debuggerLocators.errorCount).should("not.exist");
cy.wait("@postExecute");
cy.contains(debuggerLocators.errorCount, 1);
});

View File

@ -81,11 +81,12 @@ describe("Chart Widget Skeleton Loading Functionality", function() {
//Step9:
cy.get(".bp3-button-text")
.first()
.click();
.click({ force: true });
//Step10:
cy.get(".t--widget-chartwidget div[class*='bp3-skeleton']").should("exist");
/* This section is flaky hence commenting out
//Step11:
cy.reload();

View File

@ -1,10 +1,16 @@
const dsl = require("../../../../fixtures/emptyDSL.json");
const explorer = require("../../../../locators/explorerlocators.json");
const formWidgetsPage = require("../../../../locators/FormWidgets.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const publish = require("../../../../locators/publishWidgetspage.json");
describe("Dropdown Widget Functionality", function() {
before(() => {
cy.addDsl(dsl);
});
beforeEach(() => {
cy.wait(7000);
});
it("Add new dropdown widget", () => {
cy.get(explorer.addWidget).click();
@ -36,7 +42,7 @@ describe("Dropdown Widget Functionality", function() {
);
});
it("should check that more thatn empty value is not allowed in options", () => {
it("should check that more than one empty value is not allowed in options", () => {
cy.openPropertyPane("selectwidget");
cy.updateCodeInput(
".t--property-control-options",
@ -59,4 +65,61 @@ describe("Dropdown Widget Functionality", function() {
"exist",
);
});
it.skip("should check that Objects can be added to Select Widget default value", () => {
cy.openPropertyPane("selectwidget");
cy.updateCodeInput(
".t--property-control-options",
`[
{
"label": "Blue",
"value": "BLUE"
},
{
"label": "Green",
"value": "GREEN"
},
{
"label": "Red",
"value": "RED"
}
]`,
);
cy.updateCodeInput(
".t--property-control-defaultvalue",
`
{
"label": "Green",
"value": "GREEN"
}
`,
);
cy.get(".t--property-control-options .t--codemirror-has-error").should(
"not.exist",
);
cy.get(".t--property-control-defaultvalue .t--codemirror-has-error").should(
"not.exist",
);
cy.get(formWidgetsPage.dropdownDefaultButton).should("contain", "Green");
});
it("Dropdown Functionality To Check disabled Widget", function() {
cy.openPropertyPane("selectwidget");
// Disable the visible JS
cy.togglebarDisable(commonlocators.visibleCheckbox);
cy.PublishtheApp();
// Verify the disabled visible JS
cy.get(publish.selectwidget + " " + "input").should("not.exist");
cy.goToEditFromPublish();
});
it("Dropdown Functionality To UnCheck disabled Widget", function() {
cy.openPropertyPane("selectwidget");
// Check the visible JS
cy.togglebar(commonlocators.visibleCheckbox);
cy.PublishtheApp();
// Verify the checked visible JS
cy.get(publish.selectwidget).should("exist");
cy.goToEditFromPublish();
});
});

View File

@ -1,12 +1,15 @@
const dsl = require("../../../../fixtures/emptyDSL.json");
const explorer = require("../../../../locators/explorerlocators.json");
const formWidgetsPage = require("../../../../locators/FormWidgets.json");
describe("MultiSelect Widget Functionality", function() {
before(() => {
cy.addDsl(dsl);
});
it("Add new dropdown widget", () => {
beforeEach(() => {
cy.wait(7000);
});
it("Add new multiselect widget", () => {
cy.get(explorer.addWidget).click();
cy.dragAndDropToCanvas("multiselectwidgetv2", { x: 300, y: 300 });
cy.get(".t--widget-multiselectwidgetv2").should("exist");
@ -36,7 +39,7 @@ describe("MultiSelect Widget Functionality", function() {
);
});
it("should check that more thatn empty value is not allowed in options", () => {
it("should check that more that one empty value is not allowed in options", () => {
cy.openPropertyPane("multiselectwidgetv2");
cy.updateCodeInput(
".t--property-control-options",
@ -59,4 +62,44 @@ describe("MultiSelect Widget Functionality", function() {
"exist",
);
});
it("should check that Objects can be added to multiselect Widget default value", () => {
cy.openPropertyPane("multiselectwidgetv2");
cy.updateCodeInput(
".t--property-control-options",
`[
{
"label": "Blue",
"value": ""
},
{
"label": "Green",
"value": "GREEN"
},
{
"label": "Red",
"value": "RED"
}
]`,
);
cy.updateCodeInput(
".t--property-control-defaultvalue",
`[
{
"label": "Green",
"value": "GREEN"
}
]`,
);
cy.get(".t--property-control-options .t--codemirror-has-error").should(
"not.exist",
);
cy.get(".t--property-control-defaultvalue .t--codemirror-has-error").should(
"not.exist",
);
cy.wait(100);
cy.get(formWidgetsPage.multiselectwidgetv2)
.find(".rc-select-selection-item-content")
.first()
.should("have.text", "Green");
});
});

View File

@ -1,4 +1,6 @@
const dsl = require("../../../../fixtures/previewMode.json");
const commonlocators = require("../../../../locators/commonlocators.json");
const publishPage = require("../../../../locators/publishWidgetspage.json");
describe("Preview mode functionality", function() {
before(() => {
@ -26,4 +28,25 @@ describe("Preview mode functionality", function() {
`${selector}:first-of-type .t--widget-propertypane-toggle > .t--widget-name`,
).should("not.exist");
});
it("check invisible widget should not show in proview mode and should show in edit mode", function() {
cy.get(".t--switch-comment-mode-off").click();
cy.openPropertyPane("buttonwidget");
cy.UncheckWidgetProperties(commonlocators.visibleCheckbox);
// button should not show in preview mode
cy.get(".t--switch-preview-mode-toggle").click();
cy.get(`${publishPage.buttonWidget} button`).should("not.exist");
// Text widget should show
cy.get(`${publishPage.textWidget} .bp3-ui-text`).should("exist");
// button should show in edit mode
cy.get(".t--switch-comment-mode-off").click();
cy.get(`${publishPage.buttonWidget} button`).should("exist");
});
afterEach(() => {
// put your clean up code if any
});
});

View File

@ -1,7 +1,7 @@
{
"checkboxWidget": ".t--draggable-checkboxwidget",
"selectwidget": ".t--draggable-selectwidget",
"dropdownWidget": ".t--draggable-dropdownwidget",
"dropdownWidget": ".t--draggable-selectwidget",
"menuButtonWidget": ".t--draggable-menubuttonwidget",
"multiselectwidgetv2": ".t--draggable-multiselectwidgetv2",
"multiselecttreeWidget": ".t--draggable-multiselecttreewidget",

View File

@ -183,7 +183,7 @@
"widgetName":"Dropdown1",
"defaultOptionValue":"VEG",
"version":1,
"type":"DROP_DOWN_WIDGET",
"type":"SELECT_WIDGET",
"isLoading":false,
"parentColumnSpace":60.131249999999994,
"parentRowSpace":40,

View File

@ -300,7 +300,7 @@ const TextInput = forwardRef(
const [isFocused, setIsFocused] = useState(false);
const [inputValue, setInputValue] = useState(props.defaultValue);
const { trimValue = true } = props;
const { trimValue = false } = props;
const setRightSideRef = useCallback((ref: HTMLDivElement) => {
if (ref) {

View File

@ -0,0 +1,22 @@
import React from "react";
import { useSelector } from "react-redux";
import { previewModeSelector } from "selectors/editorSelectors";
type Props = {
children: React.ReactNode;
isVisible?: boolean;
};
/**
* render only visible components in preview mode
*/
function PreviewModeComponent({
children,
isVisible,
}: Props): React.ReactElement {
const isPreviewMode = useSelector(previewModeSelector);
if (!isPreviewMode || isVisible) return children as React.ReactElement;
else return <div />;
}
export default PreviewModeComponent;

View File

@ -814,7 +814,7 @@ class DatasourceRestAPIEditor extends React.Component<Props> {
value: "HEADER",
},
],
"Send client credentials with",
"Send client credentials with (on refresh token):",
"",
false,
"",

View File

@ -67,6 +67,7 @@ import {
} from "utils/ApiPaneUtils";
import { updateReplayEntity } from "actions/pageActions";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import { getDisplayFormat } from "selectors/apiPaneSelectors";
function* syncApiParamsSaga(
actionPayload: ReduxActionWithMeta<string, { field: string }>,
@ -138,7 +139,8 @@ function* handleUpdateBodyContentType(
) {
const { apiId, title } = action.payload;
const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
// this is a previous value gotten before the updated content type has been set
// this is the previous value gotten before the new content type has been set
const previousContentType =
values.actionConfiguration?.formData?.apiContentType;
@ -150,31 +152,37 @@ function* handleUpdateBodyContentType(
return;
}
// this is the update for the new api contentType
// update the api content type so it can be persisted.
// this is the update for the new apicontentType
// Quick Context: APiContentype is the field that represents the content type the user wants while in RAW mode.
// users should be able to set the content type to whatever they want.
let formData = { ...values.actionConfiguration.formData };
if (formData === undefined) formData = {};
formData["apiContentType"] = title;
formData["apiContentType"] =
title === POST_BODY_FORMAT_OPTIONS.RAW ||
title === POST_BODY_FORMAT_OPTIONS.NONE
? previousContentType
: title;
yield put(
change(API_EDITOR_FORM_NAME, "actionConfiguration.formData", formData),
);
if (displayFormatValue === POST_BODY_FORMAT_OPTIONS.RAW) {
// update the content type header if raw has been selected
yield put({
type: ReduxActionTypes.SET_EXTRA_FORMDATA,
payload: {
id: apiId,
values: {
displayFormat: {
label: displayFormatValue,
value: displayFormatValue,
},
// Quick Context: The extra formadata action is responsible for updating the current multi switch mode you see on api editor body tab
// whenever a user selects a new content type through the tab e.g application/json, this action is dispatched to update that value, which is then read in the PostDataBody file
// to show the appropriate content type section.
yield put({
type: ReduxActionTypes.SET_EXTRA_FORMDATA,
payload: {
id: apiId,
values: {
displayFormat: {
label: title,
value: title,
},
},
});
}
},
});
const headers = cloneDeep(values.actionConfiguration.headers);
@ -186,25 +194,19 @@ function* handleUpdateBodyContentType(
);
const indexToUpdate = getIndextoUpdate(headers, contentTypeHeaderIndex);
// If the user has selected "None" as the body type & there was a content-type
// If the user has selected "None" or "Raw" as the body type & there was a content-type
// header present in the API configuration, keep the previous content type header
// but if the user has selected "raw", set the content header to text/plain
// this is done to ensure user input isn't cleared off if they switch to raw or none mode.
// however if the user types in a new value, we use the updated value (formValueChangeSaga - line 426).
if (
displayFormatValue === POST_BODY_FORMAT_OPTIONS.NONE &&
(displayFormatValue === POST_BODY_FORMAT_OPTIONS.NONE ||
displayFormatValue === POST_BODY_FORMAT_OPTIONS.RAW) &&
indexToUpdate !== -1
) {
headers[indexToUpdate] = {
key: previousContentType ? CONTENT_TYPE_HEADER_KEY : "",
value: previousContentType ? previousContentType : "",
};
} else if (
displayFormatValue === POST_BODY_FORMAT_OPTIONS.RAW &&
indexToUpdate !== -1
) {
headers[indexToUpdate] = {
key: CONTENT_TYPE_HEADER_KEY,
value: POST_BODY_FORMAT_OPTIONS.RAW,
};
} else {
headers[indexToUpdate] = {
key: CONTENT_TYPE_HEADER_KEY,
@ -212,6 +214,7 @@ function* handleUpdateBodyContentType(
};
}
// update the new header values.
yield put(
change(API_EDITOR_FORM_NAME, "actionConfiguration.headers", headers),
);
@ -235,18 +238,19 @@ function* handleUpdateBodyContentType(
}
function* initializeExtraFormDataSaga() {
const state = yield select();
const { extraformData } = state.ui.apiPane;
const formData = yield select(getFormData, API_EDITOR_FORM_NAME);
const { values } = formData;
// const headers = get(values, "actionConfiguration.headers");
const apiContentType = get(
values,
"actionConfiguration.formData.apiContentType",
);
if (!extraformData[values.id]) {
yield call(setHeaderFormat, values.id, apiContentType);
// when initializing, check if theres a display format present, if not use Json display format as default.
const extraFormData = yield select(getDisplayFormat, values.id);
// as a fail safe, if no display format is present, use Raw mode
const rawApiContentType = extraFormData?.displayFormat?.value
? extraFormData?.displayFormat?.value
: POST_BODY_FORMAT_OPTIONS.RAW;
if (!extraFormData) {
yield call(setHeaderFormat, values.id, rawApiContentType);
}
}
@ -296,6 +300,7 @@ function* changeApiSaga(
function* setHeaderFormat(apiId: string, apiContentType?: string) {
// use the current apiContentType to set appropriate Headers for action
let displayFormat;
if (apiContentType) {
if (apiContentType === POST_BODY_FORMAT_OPTIONS.NONE) {
displayFormat = {
@ -336,8 +341,9 @@ export function* updateFormFields(
const value = actionPayload.payload;
log.debug("updateFormFields: " + JSON.stringify(value));
const { values } = yield select(getFormData, API_EDITOR_FORM_NAME);
let apiContentType = values.actionConfiguration.formData.apiContentType;
// get current content type of the action
let apiContentType = values.actionConfiguration.formData.apiContentType;
if (field === "actionConfiguration.httpMethod") {
const { actionConfiguration } = values;
if (!actionConfiguration.headers) return;
@ -370,14 +376,7 @@ export function* updateFormFields(
};
}
}
// change apiContentType when user changes api Http Method
yield put(
change(
API_EDITOR_FORM_NAME,
"actionConfiguration.formData.apiContentType",
apiContentType,
),
);
yield put(
change(
API_EDITOR_FORM_NAME,
@ -385,9 +384,6 @@ export function* updateFormFields(
actionConfigurationHeaders,
),
);
} else if (field.includes("actionConfiguration.headers")) {
const apiId = get(values, "id");
yield call(setHeaderFormat, apiId, apiContentType);
}
}
@ -424,29 +420,21 @@ function* formValueChangeSaga(
}),
);
// when user types a content type value, update actionConfiguration.formData.apiContent type as well.
// we don't do this initally because we want to specifically catch user editing the content-type value
if (
field === `actionConfiguration.headers[${contentTypeHeaderIndex}].value`
) {
if (
// if the value is not a registered content type, make the default apiContentType raw but don't change header
Object.values(POST_BODY_FORMAT_OPTIONS).includes(actionPayload.payload)
) {
yield put(
change(
API_EDITOR_FORM_NAME,
"actionConfiguration.formData.apiContentType",
actionPayload.payload,
),
);
} else {
yield put(
change(
API_EDITOR_FORM_NAME,
"actionConfiguration.formData.apiContentType",
POST_BODY_FORMAT_OPTIONS.RAW,
),
);
}
yield put(
change(
API_EDITOR_FORM_NAME,
"actionConfiguration.formData.apiContentType",
actionPayload.payload,
),
);
const apiId = get(values, "id");
// when the user specifically sets a new content type value, we check if the input value is a supported post body type and switch to it
// if it does not we set the default to Raw mode.
yield call(setHeaderFormat, apiId, actionPayload.payload);
}
}
yield all([

View File

@ -0,0 +1,11 @@
import { AppState } from "reducers";
type GetFormData = (
state: AppState,
apiId: string,
) => { label: string; value: string };
export const getDisplayFormat: GetFormData = (state, apiId) => {
const displayFormat = state.ui.apiPane.extraformData[apiId];
return displayFormat;
};

View File

@ -112,7 +112,7 @@ const useHorizontalResize = (
unFocus(document, window);
if (ref.current) {
const width = ref.current.getBoundingClientRect().width;
const width = ref.current.clientWidth;
const current = event.touches[0].clientX;
const positionDelta = position - current;
const widthDelta = inverse ? -positionDelta : positionDelta;

View File

@ -38,6 +38,7 @@ import OverlayCommentsWrapper from "comments/inlineComments/OverlayCommentsWrapp
import PreventInteractionsOverlay from "components/editorComponents/PreventInteractionsOverlay";
import AppsmithConsole from "utils/AppsmithConsole";
import { ENTITY_TYPE } from "entities/AppsmithConsole";
import PreviewModeComponent from "components/editorComponents/PreviewModeComponent";
/***
* BaseWidget
@ -313,12 +314,20 @@ abstract class BaseWidget<
);
}
addPreviewModeWidget(content: ReactNode): React.ReactElement {
return (
<PreviewModeComponent isVisible={this.props.isVisible}>
{content}
</PreviewModeComponent>
);
}
private getWidgetView(): ReactNode {
let content: ReactNode;
switch (this.props.renderMode) {
case RenderModes.CANVAS:
content = this.getCanvasView();
content = this.addPreviewModeWidget(content);
content = this.addPreventInteractionOverlay(content);
content = this.addOverlayComments(content);
if (!this.props.detachFromLayout) {

View File

@ -207,9 +207,7 @@ class InputWidget extends BaseInputWidget<InputWidgetProps, WidgetState> {
isTriggerProperty: false,
validation: {
type: ValidationTypes.NUMBER,
params: {
min: 1,
},
params: { min: 1, natural: true },
},
hidden: (props: InputWidgetProps) => {
return props.inputType !== InputTypes.TEXT;

View File

@ -28,7 +28,7 @@ import Icon from "components/ads/Icon";
import { Button, Classes, InputGroup } from "@blueprintjs/core";
import { WidgetContainerDiff } from "widgets/WidgetUtils";
import { Colors } from "constants/Colors";
import _ from "lodash";
import { uniqBy } from "lodash";
const menuItemSelectedIcon = (props: { isSelected: boolean }) => {
return <MenuItemCheckBox checked={props.isSelected} />;
@ -121,13 +121,12 @@ function MultiSelectComponent({
}),
);
// get unique selected values amongst SelectedAllValue and Value
const allSelectedOptions = _.uniqBy(
[...allOption, ...value],
"value",
).map((val) => ({
...val,
key: val.value,
}));
const allSelectedOptions = uniqBy([...allOption, ...value], "value").map(
(val) => ({
...val,
key: val.value,
}),
);
onChange(allSelectedOptions);
return;
}

View File

@ -6,7 +6,6 @@ export const CONFIG = {
name: "MultiSelect",
iconSVG: IconSVG,
needsMeta: true,
isFilterable: true,
defaults: {
rows: 7,
columns: 20,
@ -18,8 +17,9 @@ export const CONFIG = {
{ label: "Red", value: "RED" },
],
widgetName: "MultiSelect",
isFilterable: true,
serverSideFiltering: false,
defaultOptionValue: [{ label: "Green", value: "GREEN" }],
defaultOptionValue: ["GREEN", "RED"],
version: 1,
isRequired: false,
isDisabled: false,

View File

@ -0,0 +1,249 @@
import _ from "lodash";
import { defaultOptionValueValidation, MultiSelectWidgetProps } from ".";
describe("defaultOptionValueValidation - ", () => {
it("should get tested with empty string", () => {
const input = "";
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: [],
messages: [""],
});
});
it("should get tested with array of strings|number", () => {
const input = ["green", "red"];
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: input,
messages: [""],
});
});
it("should get tested with array json string", () => {
const input = `["green", "red"]`;
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: ["green", "red"],
messages: [""],
});
});
it("should get tested with array of object json string", () => {
const input = `[
{
"label": "green",
"value": "green"
},
{
"label": "red",
"value": "red"
}
]`;
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: [
{
label: "green",
value: "green",
},
{
label: "red",
value: "red",
},
],
messages: [""],
});
});
it("should get tested with comma seperated strings", () => {
const input = "green, red";
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: ["green", "red"],
messages: [""],
});
});
it("should get tested with simple string", () => {
const input = "green";
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: ["green"],
messages: [""],
});
});
it("should get tested with simple string", () => {
const input = `{"green"`;
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: [`{"green"`],
messages: [""],
});
});
it("should get tested with array of label, value", () => {
const input = [
{
label: "green",
value: "green",
},
{
label: "red",
value: "red",
},
];
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: [
{
label: "green",
value: "green",
},
{
label: "red",
value: "red",
},
],
messages: [""],
});
});
it("should get tested with array of invalid values", () => {
const testValues = [
[
undefined,
{
isValid: false,
parsed: [],
messages: [
"value should match: Array<string | number> | Array<{label: string, value: string | number}>",
],
},
],
[
null,
{
isValid: false,
parsed: [],
messages: [
"value should match: Array<string | number> | Array<{label: string, value: string | number}>",
],
},
],
[
true,
{
isValid: false,
parsed: [],
messages: [
"value should match: Array<string | number> | Array<{label: string, value: string | number}>",
],
},
],
[
{},
{
isValid: false,
parsed: [],
messages: [
"value should match: Array<string | number> | Array<{label: string, value: string | number}>",
],
},
],
[
[undefined],
{
isValid: false,
parsed: [],
messages: [
"value should match: Array<string | number> | Array<{label: string, value: string | number}>",
],
},
],
[
[true],
{
isValid: false,
parsed: [],
messages: [
"value should match: Array<string | number> | Array<{label: string, value: string | number}>",
],
},
],
[
["green", "green"],
{
isValid: false,
parsed: [],
messages: ["values must be unique. Duplicate values found"],
},
],
[
[
{
label: "green",
value: "green",
},
{
label: "green",
value: "green",
},
],
{
isValid: false,
parsed: [],
messages: ["path:value must be unique. Duplicate values found"],
},
],
[
[
{
label: "green",
},
{
label: "green",
},
],
{
isValid: false,
parsed: [],
messages: [
"value should match: Array<string | number> | Array<{label: string, value: string | number}>",
],
},
],
];
testValues.forEach(([input, expected]) => {
expect(
defaultOptionValueValidation(input, {} as MultiSelectWidgetProps, _),
).toEqual(expected);
});
});
});

View File

@ -2,8 +2,11 @@ import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "widgets/BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { isArray } from "lodash";
import { ValidationTypes } from "constants/WidgetValidation";
import { isArray, isString, isNumber } from "lodash";
import {
ValidationResponse,
ValidationTypes,
} from "constants/WidgetValidation";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import MultiSelectComponent from "../component";
import {
@ -12,6 +15,114 @@ import {
} from "rc-select/lib/interface/generator";
import { Layers } from "constants/Layers";
import { MinimumPopupRows, GRID_DENSITY_MIGRATION_V1 } from "widgets/constants";
import { AutocompleteDataType } from "utils/autocomplete/TernServer";
export function defaultOptionValueValidation(
value: unknown,
props: MultiSelectWidgetProps,
_: any,
): ValidationResponse {
let isValid;
let parsed;
let message = "";
/*
* Function to check if the object has `label` and `value`
*/
const hasLabelValue = (obj: any) => {
return (
_.isPlainObject(obj) &&
obj.hasOwnProperty("label") &&
obj.hasOwnProperty("value") &&
_.isString(obj.label) &&
(_.isString(obj.value) || _.isFinite(obj.value))
);
};
/*
* Function to check for duplicate values in array
*/
const hasUniqueValues = (arr: Array<string>) => {
const uniqueValues = new Set(arr);
return uniqueValues.size === arr.length;
};
/*
* When value is "['green', 'red']", "[{label: 'green', value: 'green'}]" and "green, red"
*/
if (_.isString(value) && (value as string).trim() !== "") {
try {
/*
* when value is "['green', 'red']", "[{label: 'green', value: 'green'}]"
*/
value = JSON.parse(value as string);
} catch (e) {
/*
* when value is "green, red", JSON.parse throws error
*/
const splitByComma = (value as string).split(",") || [];
value = splitByComma.map((s) => s.trim());
}
}
if (_.isString(value) && (value as string).trim() === "") {
isValid = true;
parsed = [];
message = "";
} else if (Array.isArray(value)) {
if (value.every((val) => _.isString(val) || _.isFinite(val))) {
/*
* When value is ["green", "red"]
*/
if (hasUniqueValues(value as [])) {
isValid = true;
parsed = value;
message = "";
} else {
isValid = false;
parsed = [];
message = "values must be unique. Duplicate values found";
}
} else if (value.every(hasLabelValue)) {
/*
* When value is [{label: "green", value: "red"}]
*/
if (hasUniqueValues(value.map((val) => val.value) as [])) {
isValid = true;
parsed = value;
message = "";
} else {
isValid = false;
parsed = [];
message = "path:value must be unique. Duplicate values found";
}
} else {
/*
* When value is [true, false], [undefined, undefined] etc.
*/
isValid = false;
parsed = [];
message =
"value should match: Array<string | number> | Array<{label: string, value: string | number}>";
}
} else {
/*
* When value is undefined, null, {} etc.
*/
isValid = false;
parsed = [];
message =
"value should match: Array<string | number> | Array<{label: string, value: string | number}>";
}
return {
isValid,
parsed,
messages: [message],
};
}
class MultiSelectWidget extends BaseWidget<
MultiSelectWidgetProps,
@ -66,7 +177,7 @@ class MultiSelectWidget extends BaseWidget<
EvaluationSubstitutionType.SMART_SUBSTITUTE,
},
{
helpText: "Selects the option with value by default",
helpText: "Selects the option(s) with value by default",
propertyName: "defaultOptionValue",
label: "Default Value",
controlType: "INPUT_TEXT",
@ -74,32 +185,13 @@ class MultiSelectWidget extends BaseWidget<
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.ARRAY,
type: ValidationTypes.FUNCTION,
params: {
unique: ["value"],
children: {
type: ValidationTypes.OBJECT,
params: {
required: true,
allowedKeys: [
{
name: "label",
type: ValidationTypes.TEXT,
params: {
default: "",
requiredKey: true,
},
},
{
name: "value",
type: ValidationTypes.TEXT,
params: {
default: "",
requiredKey: true,
},
},
],
},
fn: defaultOptionValueValidation,
expected: {
type: "Array of values",
example: `['option1', 'option2'] | [{ "label": "label1", "value": "value1" }]`,
autocompleteDataType: AutocompleteDataType.ARRAY,
},
},
},
@ -304,8 +396,8 @@ class MultiSelectWidget extends BaseWidget<
static getDerivedPropertiesMap() {
return {
selectedOptionLabels: `{{ this.selectedOptions ? this.selectedOptions.map((o) => o.label ) : [] }}`,
selectedOptionValues: `{{ this.selectedOptions ? this.selectedOptions.map((o) => o.value ) : [] }}`,
selectedOptionLabels: `{{ this.selectedOptions ? this.selectedOptions.map((o) => _.isNil(o.label) ? o : o.label ) : [] }}`,
selectedOptionValues: `{{ this.selectedOptions ? this.selectedOptions.map((o) => _.isNil(o.value) ? o : o.value ) : [] }}`,
isValid: `{{this.isRequired ? !!this.selectedOptionValues && this.selectedOptionValues.length > 0 : true}}`,
isDirty: `{{ ((array1, array2) => {if (array1.length === array2.length) {return !array1.map((o) => o.value).every(element => array2.includes(element));} return true;})(this.defaultOptionValue, this.selectedOptionValues); }}`,
};
@ -329,7 +421,13 @@ class MultiSelectWidget extends BaseWidget<
const options = isArray(this.props.options) ? this.props.options : [];
const dropDownWidth = MinimumPopupRows * this.props.parentColumnSpace;
const { componentWidth } = this.getComponentDimensions();
const values: LabelValueType[] = this.props.selectedOptions
? this.props.selectedOptions.map((o) =>
isString(o) || isNumber(o)
? { label: o, value: o }
: { label: o.label, value: o.value },
)
: [];
return (
<MultiSelectComponent
allowSelectAll={this.props.allowSelectAll}
@ -358,7 +456,7 @@ class MultiSelectWidget extends BaseWidget<
options={options}
placeholder={this.props.placeholderText as string}
serverSideFiltering={this.props.serverSideFiltering}
value={this.props.selectedOptions ?? []}
value={values}
widgetId={this.props.widgetId}
width={componentWidth}
/>
@ -366,6 +464,10 @@ class MultiSelectWidget extends BaseWidget<
}
onOptionChange = (value: DefaultValueType) => {
if (!this.props.isDirty) {
this.props.updateWidgetMetaProperty("isDirty", true);
}
this.props.updateWidgetMetaProperty("selectedOptions", value, {
triggerPropertyName: "onOptionChange",
dynamicString: this.props.onOptionChange,

View File

@ -12,6 +12,7 @@ import {
BlueprintCSSTransform,
createGlobalStyle,
} from "constants/DefaultTheme";
import { isEmptyOrNill } from ".";
export const TextLabelWrapper = styled.div<{
compactMode: boolean;
@ -145,7 +146,8 @@ export const StyledSingleDropDown = styled(SingleDropDown)<{
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
color: ${(props) => (props.value ? Colors.GREY_10 : Colors.GREY_6)};
color: ${(props) =>
!isEmptyOrNill(props.value) ? Colors.GREY_10 : Colors.GREY_6};
}
&& {
.${Classes.ICON} {

View File

@ -3,7 +3,7 @@ import { ComponentProps } from "widgets/BaseComponent";
import { MenuItem, Button, Classes } from "@blueprintjs/core";
import { DropdownOption } from "../constants";
import { IItemRendererProps } from "@blueprintjs/select";
import _ from "lodash";
import { debounce, findIndex, isEmpty, isNil } from "lodash";
import "../../../../node_modules/@blueprintjs/select/lib/css/blueprint-select.css";
import { Colors } from "constants/Colors";
import { TextSize } from "constants/WidgetConstants";
@ -19,6 +19,7 @@ import {
import Fuse from "fuse.js";
import { WidgetContainerDiff } from "widgets/WidgetUtils";
import Icon, { IconSize } from "components/ads/Icon";
import { isString } from "../../../utils/helpers";
const FUSE_OPTIONS = {
shouldSort: true,
@ -29,6 +30,10 @@ const FUSE_OPTIONS = {
keys: ["label", "value"],
};
export const isEmptyOrNill = (value: any) => {
return isNil(value) || (isString(value) && value === "");
};
const DEBOUNCE_TIMEOUT = 800;
interface SelectComponentState {
@ -59,7 +64,7 @@ class SelectComponent extends React.Component<
handleActiveItemChange = (activeItem: DropdownOption | null) => {
// find new index from options
const activeItemIndex = _.findIndex(this.props.options, [
const activeItemIndex = findIndex(this.props.options, [
"label",
activeItem?.label,
]);
@ -77,20 +82,21 @@ class SelectComponent extends React.Component<
widgetId,
} = this.props;
// active focused item
const activeItem = !_.isEmpty(this.props.options)
const activeItem = !isEmpty(this.props.options)
? this.props.options[this.state.activeItemIndex]
: undefined;
// get selected option label from selectedIndex
const selectedOption =
!_.isEmpty(this.props.options) &&
!isEmpty(this.props.options) &&
this.props.selectedIndex !== undefined &&
this.props.selectedIndex > -1
? this.props.options[this.props.selectedIndex].label
: this.props.label;
// for display selected option, there is no separate option to show placeholder
const value = selectedOption
? selectedOption
: this.props.placeholder || "-- Select --";
const value =
!isNil(selectedOption) && selectedOption !== ""
? selectedOption
: this.props.placeholder || "-- Select --";
return (
<DropdownContainer compactMode={compactMode}>
@ -142,7 +148,7 @@ class SelectComponent extends React.Component<
onClose: () => {
if (!this.props.selectedIndex) return;
return this.handleActiveItemChange(
this.props.options[this.props.selectedIndex as number],
this.props.options[this.props.selectedIndex],
);
},
modifiers: {
@ -160,7 +166,7 @@ class SelectComponent extends React.Component<
disabled={this.props.disabled}
rightIcon={
<StyledDiv>
{this.props.value ? (
{!isEmptyOrNill(this.props.value) ? (
<Icon
className="dropdown-icon cancel-icon"
fillColor={
@ -201,7 +207,7 @@ class SelectComponent extends React.Component<
};
isOptionSelected = (selectedOption: DropdownOption) => {
const optionIndex = _.findIndex(this.props.options, (option) => {
const optionIndex = findIndex(this.props.options, (option) => {
return option.value === selectedOption.value;
});
return optionIndex === this.props.selectedIndex;
@ -211,7 +217,7 @@ class SelectComponent extends React.Component<
if (!this.props.serverSideFiltering) return;
return this.serverSideSearch(filterValue);
};
serverSideSearch = _.debounce((filterValue: string) => {
serverSideSearch = debounce((filterValue: string) => {
this.props.onFilterChange(filterValue);
}, DEBOUNCE_TIMEOUT);

View File

@ -18,9 +18,9 @@ export const CONFIG = {
],
serverSideFiltering: false,
widgetName: "Select",
defaultOptionValue: { label: "Green", value: "GREEN" },
defaultOptionValue: "GREEN",
version: 1,
isFilterable: false,
isFilterable: true,
isRequired: false,
isDisabled: false,
animateLoading: true,

View File

@ -0,0 +1,109 @@
import _ from "lodash";
import { SelectWidgetProps, defaultOptionValueValidation } from ".";
describe("defaultOptionValueValidation - ", () => {
it("should get tested with simple string", () => {
const input = "";
expect(
defaultOptionValueValidation(input, {} as SelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: "",
messages: [""],
});
});
it("should get tested with simple string", () => {
const input = "green";
expect(
defaultOptionValueValidation(input, {} as SelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: "green",
messages: [""],
});
});
it("should get tested with plain object", () => {
const input = {
label: "green",
value: "green",
};
expect(
defaultOptionValueValidation(input, {} as SelectWidgetProps, _),
).toEqual({
isValid: true,
parsed: {
label: "green",
value: "green",
},
messages: [""],
});
});
it("should get tested with invalid values", () => {
const testValues = [
[
undefined,
{
isValid: false,
parsed: {},
messages: [
`value does not evaluate to type: string | { "label": "label1", "value": "value1" }`,
],
},
],
[
null,
{
isValid: false,
parsed: {},
messages: [
`value does not evaluate to type: string | { "label": "label1", "value": "value1" }`,
],
},
],
[
[],
{
isValid: false,
parsed: {},
messages: [
`value does not evaluate to type: string | { "label": "label1", "value": "value1" }`,
],
},
],
[
true,
{
isValid: false,
parsed: {},
messages: [
`value does not evaluate to type: string | { "label": "label1", "value": "value1" }`,
],
},
],
[
{
label: "green",
},
{
isValid: false,
parsed: {},
messages: [
`value does not evaluate to type: string | { "label": "label1", "value": "value1" }`,
],
},
],
];
testValues.forEach(([input, expected]) => {
expect(
defaultOptionValueValidation(input, {} as SelectWidgetProps, _),
).toEqual(expected);
});
});
});

View File

@ -3,13 +3,70 @@ import BaseWidget, { WidgetProps, WidgetState } from "../../BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import SelectComponent from "../component";
import _ from "lodash";
import { DropdownOption } from "../constants";
import { ValidationTypes } from "constants/WidgetValidation";
import {
ValidationResponse,
ValidationTypes,
} from "constants/WidgetValidation";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import { MinimumPopupRows, GRID_DENSITY_MIGRATION_V1 } from "widgets/constants";
import { AutocompleteDataType } from "utils/autocomplete/TernServer";
import { findIndex, isArray, isNumber, isString } from "lodash";
export function defaultOptionValueValidation(
value: unknown,
props: SelectWidgetProps,
_: any,
): ValidationResponse {
let isValid;
let parsed;
let message = "";
/*
* Function to check if the object has `label` and `value`
*/
const hasLabelValue = (obj: any) => {
return (
_.isPlainObject(value) &&
obj.hasOwnProperty("label") &&
obj.hasOwnProperty("value") &&
_.isString(obj.label) &&
(_.isString(obj.value) || _.isFinite(obj.value))
);
};
/*
* When value is "{label: 'green', value: 'green'}"
*/
if (typeof value === "string") {
try {
value = JSON.parse(value);
} catch (e) {}
}
if (_.isString(value) || _.isFinite(value) || hasLabelValue(value)) {
/*
* When value is "", "green", 444, {label: "green", value: "green"}
*/
isValid = true;
parsed = value;
} else {
isValid = false;
parsed = {};
message = `value does not evaluate to type: string | { "label": "label1", "value": "value1" }`;
}
return {
isValid,
parsed,
messages: [message],
};
}
class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
constructor(props: SelectWidgetProps) {
super(props);
}
static getPropertyPaneConfig() {
return [
{
@ -21,7 +78,7 @@ class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
propertyName: "options",
label: "Options",
controlType: "INPUT_TEXT",
placeholderText: '[{ "label": "Option1", "value": "Option2" }]',
placeholderText: '[{ "label": "label1", "value": "value1" }]',
isBindProperty: true,
isTriggerProperty: false,
validation: {
@ -60,34 +117,24 @@ class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
{
helpText: "Selects the option with value by default",
propertyName: "defaultOptionValue",
label: "Default Option",
label: "Default Value",
controlType: "INPUT_TEXT",
placeholderText: '{ "label": "Option1", "value": "Option2" }',
placeholderText: '{ "label": "label1", "value": "value1" }',
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.OBJECT,
type: ValidationTypes.FUNCTION,
params: {
allowedKeys: [
{
name: "label",
type: ValidationTypes.TEXT,
params: {
default: "",
requiredKey: true,
},
},
{
name: "value",
type: ValidationTypes.TEXT,
params: {
default: "",
requiredKey: true,
},
},
],
fn: defaultOptionValueValidation,
expected: {
type: 'value1 or { "label": "label1", "value": "value1" }',
example: `value1 | { "label": "label1", "value": "value1" }`,
autocompleteDataType: AutocompleteDataType.STRING,
},
},
},
evaluationSubstitutionType:
EvaluationSubstitutionType.SMART_SUBSTITUTE,
},
{
helpText: "Sets a Label Text",
@ -273,57 +320,49 @@ class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
];
}
static getDerivedPropertiesMap() {
return {
isValid: `{{this.isRequired ? !!this.selectedOptionValue || this.selectedOptionValue === 0 : true}}`,
selectedOptionLabel: `{{ this.optionValue.label ?? this.optionValue.value }}`,
selectedOptionValue: `{{ this.optionValue.value }}`,
isDirty: `{{ this.optionValue.value !== this.defaultValue.value }}`,
};
}
static getDefaultPropertiesMap(): Record<string, string> {
return {
defaultValue: "defaultOptionValue",
optionValue: "defaultOptionValue",
value: "defaultOptionValue",
label: "defaultOptionValue",
filterText: "",
};
}
static getMetaPropertiesMap(): Record<string, any> {
return {
defaultValue: undefined,
optionValue: undefined,
value: undefined,
label: undefined,
filterText: "",
};
}
static getDerivedPropertiesMap() {
return {
isValid: `{{this.isRequired ? !!this.selectedOptionValue || this.selectedOptionValue === 0 : true}}`,
selectedOptionLabel: `{{(()=>{const label = _.isPlainObject(this.label) ? this.label?.label : this.label; return label; })()}}`,
selectedOptionValue: `{{(()=>{const value = _.isPlainObject(this.value) ? this.value?.value : this.value; return value; })()}}`,
isDirty: `{{ this.value !== this.defaultValue }}`,
};
}
componentDidMount() {
super.componentDidMount();
this.changeSelectedOption();
}
componentDidUpdate(prevProps: SelectWidgetProps): void {
// removing selectedOptionValue if defaultValueChanges
if (
prevProps.defaultOptionValue?.value !==
this.props.defaultOptionValue?.value ||
prevProps.option !== this.props.option
) {
this.changeSelectedOption();
}
}
changeSelectedOption = () => {
this.props.updateWidgetMetaProperty("optionValue", this.props.optionValue);
};
isStringOrNumber = (value: any): value is string | number =>
isString(value) || isNumber(value);
getPageView() {
const options = _.isArray(this.props.options) ? this.props.options : [];
const options = isArray(this.props.options) ? this.props.options : [];
const isInvalid =
"isValid" in this.props && !this.props.isValid && !!this.props.isDirty;
const dropDownWidth = MinimumPopupRows * this.props.parentColumnSpace;
const selectedIndex = _.findIndex(this.props.options, {
const selectedIndex = findIndex(this.props.options, {
value: this.props.selectedOptionValue,
});
const { componentHeight, componentWidth } = this.getComponentDimensions();
return (
<SelectComponent
@ -342,7 +381,7 @@ class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
isFilterable={this.props.isFilterable}
isLoading={this.props.isLoading}
isValid={this.props.isValid}
label={this.props.optionValue?.label}
label={this.props.selectedOptionLabel}
labelStyle={this.props.labelStyle}
labelText={this.props.labelText}
labelTextColor={this.props.labelTextColor}
@ -353,7 +392,7 @@ class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
placeholder={this.props.placeholderText}
selectedIndex={selectedIndex > -1 ? selectedIndex : undefined}
serverSideFiltering={this.props.serverSideFiltering}
value={this.props.optionValue?.value}
value={this.props.selectedOptionValue}
widgetId={this.props.widgetId}
width={componentWidth}
/>
@ -369,9 +408,11 @@ class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
isChanged = !(this.props.selectedOptionValue === selectedOption.value);
}
if (isChanged) {
this.props.updateWidgetMetaProperty("optionValue", selectedOption, {
this.props.updateWidgetMetaProperty("label", selectedOption.label ?? "");
this.props.updateWidgetMetaProperty("value", selectedOption.value ?? "", {
triggerPropertyName: "onOptionChange",
dynamicString: this.props.onOptionChange as string,
dynamicString: this.props.onOptionChange,
event: {
type: EventType.ON_OPTION_CHANGE,
},
@ -379,16 +420,29 @@ class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
}
};
changeSelectedOption = () => {
const label = this.isStringOrNumber(this.props.label)
? this.props.label
: this.props.label?.label;
const value = this.isStringOrNumber(this.props.value)
? this.props.value
: this.props.value?.value;
this.props.updateWidgetMetaProperty("value", value);
this.props.updateWidgetMetaProperty("label", label);
};
onFilterChange = (value: string) => {
this.props.updateWidgetMetaProperty("filterText", value);
super.executeAction({
triggerPropertyName: "onFilterUpdate",
dynamicString: this.props.onFilterUpdate,
event: {
type: EventType.ON_FILTER_UPDATE,
},
});
if (this.props.onFilterUpdate && this.props.serverSideFiltering) {
super.executeAction({
triggerPropertyName: "onFilterUpdate",
dynamicString: this.props.onFilterUpdate,
event: {
type: EventType.ON_FILTER_UPDATE,
},
});
}
};
static getWidgetType(): WidgetType {
@ -398,13 +452,12 @@ class SelectWidget extends BaseWidget<SelectWidgetProps, WidgetState> {
export interface SelectWidgetProps extends WidgetProps {
placeholderText?: string;
label?: string;
selectedIndex?: number;
selectedOption: DropdownOption;
options?: DropdownOption[];
onOptionChange?: string;
defaultOptionValue?: { label?: string; value?: string };
value?: string;
defaultOptionValue?: any;
value?: any;
label?: any;
isRequired: boolean;
isFilterable: boolean;
defaultValue: string;

View File

@ -66,27 +66,20 @@ const WIDGET_CONFIG_MAP: WidgetTypeConfigMap = {
},
metaProperties: {},
},
DROP_DOWN_WIDGET: {
SELECT_WIDGET: {
defaultProperties: {
selectedOptionValue: "defaultOptionValue",
selectedOptionValueArr: "defaultOptionValue",
selectedOption: "defaultOptionValue",
filterText: "",
},
derivedProperties: {
isValid:
"{{this.isRequired ? this.selectionType === 'SINGLE_SELECT' ? !!this.selectedOption : !!this.selectedIndexArr && this.selectedIndexArr.length > 0 : true}}",
selectedOption:
"{{ this.selectionType === 'SINGLE_SELECT' ? _.find(this.options, { value: this.selectedOptionValue }) : undefined}}",
selectedOptionArr:
'{{this.selectionType === "MULTI_SELECT" ? this.options.filter(opt => _.includes(this.selectedOptionValueArr, opt.value)) : undefined}}',
selectedIndex:
"{{ _.findIndex(this.options, { value: this.selectedOption.value } ) }}",
selectedIndexArr:
"{{ this.selectedOptionValueArr.map(o => _.findIndex(this.options, { value: o })) }}",
value:
"{{ this.selectionType === 'SINGLE_SELECT' ? this.selectedOptionValue : this.selectedOptionValueArr }}",
selectedOptionValues: "{{ this.selectedOptionValueArr }}",
selectedOptionLabel: `{{_.isPlainObject(this.selectedOption) ? this.selectedOption?.label : this.selectedOption}}`,
selectedOptionValue: `{{_.isPlainObject(this.selectedOption) ? this.selectedOption?.value : this.selectedOption}}`,
isValid: `{{this.isRequired ? !!this.selectedOptionValue || this.selectedOptionValue === 0 : true}}`,
},
metaProperties: {
selectedOption: undefined,
filterText: "",
},
metaProperties: {},
},
RADIO_GROUP_WIDGET: {
defaultProperties: {
@ -278,25 +271,25 @@ const mockDerived = jest.spyOn(WidgetFactory, "getWidgetDerivedPropertiesMap");
const dependencyMap = {
Dropdown1: [
"Dropdown1.defaultOptionValue",
"Dropdown1.filterText",
"Dropdown1.isValid",
"Dropdown1.selectedIndex",
"Dropdown1.selectedIndexArr",
"Dropdown1.meta",
"Dropdown1.selectedOption",
"Dropdown1.selectedOptionArr",
"Dropdown1.selectedOptionLabel",
"Dropdown1.selectedOptionValue",
"Dropdown1.selectedOptionValueArr",
"Dropdown1.selectedOptionValues",
"Dropdown1.value",
],
"Dropdown1.isValid": [],
"Dropdown1.selectedIndex": [],
"Dropdown1.selectedIndexArr": [],
"Dropdown1.selectedOption": [],
"Dropdown1.selectedOptionArr": [],
"Dropdown1.selectedOptionValue": ["Dropdown1.defaultOptionValue"],
"Dropdown1.selectedOptionValueArr": ["Dropdown1.defaultOptionValue"],
"Dropdown1.selectedOptionValues": [],
"Dropdown1.value": [],
"Dropdown1.filterText": ["Dropdown1.meta.filterText"],
"Dropdown1.meta": [
"Dropdown1.meta.filterText",
"Dropdown1.meta.selectedOption",
],
"Dropdown1.selectedOption": [
"Dropdown1.defaultOptionValue",
"Dropdown1.meta.selectedOption",
],
"Dropdown1.selectedOptionLabel": [],
"Dropdown1.selectedOptionValue": [],
Table1: [
"Table1.defaultSearchText",
"Table1.defaultSelectedRow",
@ -394,7 +387,7 @@ describe("DataTreeEvaluator", () => {
value: "valueTest2",
},
],
type: "DROP_DOWN_WIDGET",
type: "SELECT_WIDGET",
},
{},
),
@ -492,7 +485,7 @@ describe("DataTreeEvaluator", () => {
value: "valueTest2",
},
],
type: "DROP_DOWN_WIDGET",
type: "SELECT_WIDGET",
bindingPaths: {
options: EvaluationSubstitutionType.TEMPLATE,
defaultOptionValue: EvaluationSubstitutionType.TEMPLATE,
@ -501,11 +494,8 @@ describe("DataTreeEvaluator", () => {
isDisabled: EvaluationSubstitutionType.TEMPLATE,
isValid: EvaluationSubstitutionType.TEMPLATE,
selectedOption: EvaluationSubstitutionType.TEMPLATE,
selectedOptionArr: EvaluationSubstitutionType.TEMPLATE,
selectedIndex: EvaluationSubstitutionType.TEMPLATE,
selectedIndexArr: EvaluationSubstitutionType.TEMPLATE,
value: EvaluationSubstitutionType.TEMPLATE,
selectedOptionValues: EvaluationSubstitutionType.TEMPLATE,
selectedOptionValue: EvaluationSubstitutionType.TEMPLATE,
selectedOptionLabel: EvaluationSubstitutionType.TEMPLATE,
},
},
};

View File

@ -34,7 +34,7 @@ export const WidgetTypeFactories: Record<string, any> = {
DATE_PICKER_WIDGET: OldDatepickerFactory,
DATE_PICKER_WIDGET2: DatepickerFactory,
TABLE_WIDGET: TableFactory,
DROP_DOWN_WIDGET: DropdownFactory,
SELECT_WIDGET: DropdownFactory,
CHECKBOX_WIDGET: CheckboxFactory,
RADIO_GROUP_WIDGET: RadiogroupFactory,
TABS_WIDGET: TabsFactory,

View File

@ -8,6 +8,7 @@ import com.appsmith.server.domains.CommentThread;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Page;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.User;
import lombok.Getter;
@ -74,13 +75,15 @@ public enum AclPermission {
READ_DATASOURCES("read:datasources", Datasource.class),
EXECUTE_DATASOURCES("execute:datasources", Datasource.class),
COMMENT_ON_THREAD("canComment:commentThreads", CommentThread.class),
READ_THREAD("read:commentThreads", CommentThread.class),
MANAGE_THREAD("manage:commentThreads", CommentThread.class),
COMMENT_ON_THREADS("canComment:commentThreads", CommentThread.class),
READ_THREADS("read:commentThreads", CommentThread.class),
MANAGE_THREADS("manage:commentThreads", CommentThread.class),
READ_COMMENT("read:comments", Comment.class),
MANAGE_COMMENT("manage:comments", Comment.class),
READ_COMMENTS("read:comments", Comment.class),
MANAGE_COMMENTS("manage:comments", Comment.class),
READ_THEMES("read:themes", Theme.class),
MANAGE_THEMES("manage:themes", Theme.class),
;
private final String value;

View File

@ -20,7 +20,7 @@ import java.util.Set;
import java.util.stream.Collectors;
import static com.appsmith.server.acl.AclPermission.COMMENT_ON_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.COMMENT_ON_THREAD;
import static com.appsmith.server.acl.AclPermission.COMMENT_ON_THREADS;
import static com.appsmith.server.acl.AclPermission.EXECUTE_ACTIONS;
import static com.appsmith.server.acl.AclPermission.EXECUTE_DATASOURCES;
import static com.appsmith.server.acl.AclPermission.EXPORT_APPLICATIONS;
@ -30,6 +30,7 @@ import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES;
import static com.appsmith.server.acl.AclPermission.MANAGE_ORGANIZATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
import static com.appsmith.server.acl.AclPermission.MANAGE_THEMES;
import static com.appsmith.server.acl.AclPermission.MANAGE_USERS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_EXPORT_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_MANAGE_APPLICATIONS;
@ -38,11 +39,12 @@ import static com.appsmith.server.acl.AclPermission.ORGANIZATION_READ_APPLICATIO
import static com.appsmith.server.acl.AclPermission.PUBLISH_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.READ_ACTIONS;
import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.READ_COMMENT;
import static com.appsmith.server.acl.AclPermission.READ_COMMENTS;
import static com.appsmith.server.acl.AclPermission.READ_DATASOURCES;
import static com.appsmith.server.acl.AclPermission.READ_ORGANIZATIONS;
import static com.appsmith.server.acl.AclPermission.READ_PAGES;
import static com.appsmith.server.acl.AclPermission.READ_THREAD;
import static com.appsmith.server.acl.AclPermission.READ_THEMES;
import static com.appsmith.server.acl.AclPermission.READ_THREADS;
import static com.appsmith.server.acl.AclPermission.READ_USERS;
import static com.appsmith.server.acl.AclPermission.USER_MANAGE_ORGANIZATIONS;
import static com.appsmith.server.acl.AclPermission.USER_READ_ORGANIZATIONS;
@ -81,6 +83,7 @@ public class PolicyGeneratorCE {
createPagePolicyGraph();
createActionPolicyGraph();
createCommentPolicyGraph();
createThemePolicyGraph();
}
/**
@ -142,11 +145,17 @@ public class PolicyGeneratorCE {
}
private void createCommentPolicyGraph() {
hierarchyGraph.addEdge(COMMENT_ON_APPLICATIONS, COMMENT_ON_THREAD);
hierarchyGraph.addEdge(COMMENT_ON_APPLICATIONS, COMMENT_ON_THREADS);
lateralGraph.addEdge(COMMENT_ON_THREAD, READ_THREAD);
lateralGraph.addEdge(COMMENT_ON_THREADS, READ_THREADS);
hierarchyGraph.addEdge(COMMENT_ON_THREAD, READ_COMMENT);
hierarchyGraph.addEdge(COMMENT_ON_THREADS, READ_COMMENTS);
}
private void createThemePolicyGraph() {
hierarchyGraph.addEdge(MANAGE_APPLICATIONS, MANAGE_THEMES);
hierarchyGraph.addEdge(READ_APPLICATIONS, READ_THEMES);
lateralGraph.addEdge(MANAGE_THEMES, READ_THEMES);
}
public Set<Policy> getLateralPolicies(AclPermission permission, Set<String> userNames, Class<? extends BaseDomain> destinationEntity) {

View File

@ -106,5 +106,6 @@ public class FieldName {
public static final String ACTION_LIST = "actionList";
public static final String ACTION_COLLECTION_LIST = "actionCollectionList";
public static final String DECRYPTED_FIELDS = "decryptedFields";
public static final String THEME = "theme";
}

View File

@ -4,18 +4,23 @@ import com.appsmith.server.constants.Url;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.dtos.ResponseDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.services.ThemeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.validation.Valid;
import java.util.List;
@Slf4j
@RequestMapping(Url.THEME_URL)
@ -24,15 +29,38 @@ public class ThemeControllerCE extends BaseController<ThemeService, Theme, Strin
super(themeService);
}
@Override
public Mono<ResponseDTO<Theme>> create(Theme resource, String originHeader, ServerWebExchange exchange) {
throw new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION);
}
@GetMapping("applications/{applicationId}")
public Mono<ResponseDTO<Theme>> getThemes(@PathVariable String applicationId, @RequestParam(required = false, defaultValue = "EDIT") ApplicationMode mode) {
public Mono<ResponseDTO<List<Theme>>> getApplicationThemes(@PathVariable String applicationId) {
return service.getApplicationThemes(applicationId).collectList()
.map(themes -> new ResponseDTO<>(HttpStatus.OK.value(), themes, null));
}
@GetMapping("applications/{applicationId}/current")
public Mono<ResponseDTO<Theme>> getCurrentTheme(@PathVariable String applicationId, @RequestParam(required = false, defaultValue = "EDIT") ApplicationMode mode) {
return service.getApplicationTheme(applicationId, mode)
.map(theme -> new ResponseDTO<>(HttpStatus.OK.value(), theme, null));
}
@PostMapping("applications/{applicationId}")
@PutMapping("applications/{applicationId}")
public Mono<ResponseDTO<Theme>> updateTheme(@PathVariable String applicationId, @Valid @RequestBody Theme resource) {
return service.updateTheme(applicationId, resource)
.map(theme -> new ResponseDTO<>(HttpStatus.OK.value(), theme, null));
}
@PatchMapping("applications/{applicationId}")
public Mono<ResponseDTO<Theme>> publishCurrentTheme(@PathVariable String applicationId, @RequestBody Theme resource) {
return service.persistCurrentTheme(applicationId, resource)
.map(theme -> new ResponseDTO<>(HttpStatus.OK.value(), theme, null));
}
@PatchMapping("{themeId}")
public Mono<ResponseDTO<Theme>> updateName(@PathVariable String themeId, @Valid @RequestBody Theme resource) {
return service.updateName(themeId, resource)
.map(theme -> new ResponseDTO<>(HttpStatus.OK.value(), theme, null));
}
}

View File

@ -2,7 +2,6 @@ package com.appsmith.server.domains;
import com.appsmith.external.models.BaseDomain;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.gson.annotations.SerializedName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
@ -11,7 +10,6 @@ import lombok.Setter;
import org.springframework.data.mongodb.core.mapping.Document;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
@Getter
@ -23,23 +21,15 @@ public class Theme extends BaseDomain {
@NotNull
private String name;
private Config config;
private Properties properties;
private Map<String, WidgetStyle> stylesheet;
private String applicationId;
private String organizationId;
private Object config;
private Object properties;
private Map<String, Object> stylesheet;
@JsonProperty("isSystemTheme") // manually setting property name to make sure it's compatible with Gson
private boolean isSystemTheme = false; // should be false by default
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Properties {
private Colors colors;
private BorderRadiusProperties borderRadius;
private BoxShadowProperties boxShadow;
private FontFamilyProperties fontFamily;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ -47,87 +37,4 @@ public class Theme extends BaseDomain {
private String primaryColor;
private String backgroundColor;
}
@Data
public static class Config {
private Colors colors;
private BorderRadius borderRadius;
private BoxShadow boxShadow;
private FontFamily fontFamily;
}
@Data
public static class ResponsiveAttributes {
@JsonProperty("none")
@SerializedName("none")
private String noneValue;
@JsonProperty("DEFAULT")
@SerializedName("DEFAULT")
private String defaultValue;
@JsonProperty("md")
@SerializedName("md")
private String mdValue;
@JsonProperty("lg")
@SerializedName("lg")
private String lgValue;
@JsonProperty("xl")
@SerializedName("xl")
private String xlValue;
@JsonProperty("2xl")
@SerializedName("2xl")
private String doubleXlValue;
@JsonProperty("3xl")
@SerializedName("3xl")
private String tripleXlValue;
@JsonProperty("full")
@SerializedName("full")
private String fullValue;
}
@Data
public static class BorderRadius {
private ResponsiveAttributes appBorderRadius;
}
@Data
public static class BoxShadow {
private ResponsiveAttributes appBoxShadow;
}
@Data
public static class FontFamily {
private List<String> appFont;
}
@Data
public static class FontFamilyProperties {
private String appFont;
}
@Data
public static class WidgetStyle {
private String backgroundColor;
private String borderRadius;
private String boxShadow;
private String primaryColor;
private String menuColor;
private String buttonColor;
}
@Data
public static class BorderRadiusProperties {
private String appBorderRadius;
}
@Data
public static class BoxShadowProperties {
private String appBoxShadow;
}
}

View File

@ -10,6 +10,7 @@ import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.CommentThread;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.User;
import com.appsmith.server.repositories.ActionCollectionRepository;
import com.appsmith.server.repositories.ApplicationRepository;
@ -17,9 +18,11 @@ import com.appsmith.server.repositories.CommentThreadRepository;
import com.appsmith.server.repositories.DatasourceRepository;
import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.repositories.ThemeRepository;
import lombok.AllArgsConstructor;
import org.apache.commons.collections.CollectionUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -35,6 +38,7 @@ import java.util.function.Function;
import java.util.stream.Collectors;
import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES;
import static com.appsmith.server.acl.AclPermission.READ_THEMES;
@Component
@AllArgsConstructor
@ -47,6 +51,7 @@ public class PolicyUtils {
private final NewActionRepository newActionRepository;
private final CommentThreadRepository commentThreadRepository;
private final ActionCollectionRepository actionCollectionRepository;
private final ThemeRepository themeRepository;
public <T extends BaseDomain> T addPoliciesToExistingObject(Map<String, Policy> policyMap, T obj) {
// Making a deep copy here so we don't modify the `policyMap` object.
@ -231,12 +236,37 @@ public class PolicyUtils {
.saveAll(updatedPages));
}
public Flux<Theme> updateThemePolicies(Application application, Map<String, Policy> themePolicyMap, boolean addPolicyToObject) {
Flux<Theme> applicationThemes = themeRepository.getApplicationThemes(application.getId(), READ_THEMES);
if(StringUtils.hasLength(application.getEditModeThemeId())) {
applicationThemes = applicationThemes.concatWith(
themeRepository.findById(application.getEditModeThemeId(), READ_THEMES)
);
}
if(StringUtils.hasLength(application.getPublishedModeThemeId())) {
applicationThemes = applicationThemes.concatWith(
themeRepository.findById(application.getPublishedModeThemeId(), READ_THEMES)
);
}
return applicationThemes
.filter(theme -> !theme.isSystemTheme()) // skip the system themes
.map(theme -> {
if (addPolicyToObject) {
return addPoliciesToExistingObject(themePolicyMap, theme);
} else {
return removePoliciesFromExistingObject(themePolicyMap, theme);
}
})
.collectList()
.flatMapMany(themeRepository::saveAll);
}
public Flux<CommentThread> updateCommentThreadPermissions(
String applicationId, Map<String, Policy> commentThreadPolicyMap, String username, boolean addPolicyToObject) {
return
// fetch comment threads with read permissions
commentThreadRepository.findByApplicationId(applicationId, AclPermission.READ_THREAD)
commentThreadRepository.findByApplicationId(applicationId, AclPermission.READ_THREADS)
.switchIfEmpty(Mono.empty())
.map(thread -> {
if(!Boolean.TRUE.equals(thread.getIsPrivate())) {

View File

@ -138,6 +138,7 @@ import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_EXPORT_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.ORGANIZATION_INVITE_USERS;
import static com.appsmith.server.acl.AclPermission.READ_ACTIONS;
import static com.appsmith.server.acl.AclPermission.READ_THEMES;
import static com.appsmith.server.constants.FieldName.DEFAULT_RESOURCES;
import static com.appsmith.server.constants.FieldName.DYNAMIC_TRIGGER_PATH_LIST;
import static com.appsmith.server.helpers.CollectionUtils.isNullOrEmpty;
@ -4744,37 +4745,6 @@ public class DatabaseChangelog {
mongockTemplate.save(firestorePlugin);
}
@ChangeSet(order = "108", id = "create-system-themes", author = "")
public void createSystemThemes(MongockTemplate mongockTemplate) throws IOException {
Index uniqueApplicationIdIndex = new Index()
.on(fieldName(QTheme.theme.isSystemTheme), Sort.Direction.ASC)
.named("system_theme_index");
ensureIndexes(mongockTemplate, Theme.class, uniqueApplicationIdIndex);
final String themesJson = StreamUtils.copyToString(
new DefaultResourceLoader().getResource("system-themes.json").getInputStream(),
Charset.defaultCharset()
);
Theme[] themes = new Gson().fromJson(themesJson, Theme[].class);
Theme legacyTheme = null;
for (Theme theme : themes) {
theme.setSystemTheme(true);
Theme savedTheme = mongockTemplate.save(theme);
if(savedTheme.getName().equalsIgnoreCase(Theme.LEGACY_THEME_NAME)) {
legacyTheme = savedTheme;
}
}
// migrate all applications and set legacy theme to them in both mode
Update update = new Update().set(fieldName(QApplication.application.publishedModeThemeId), legacyTheme.getId())
.set(fieldName(QApplication.application.editModeThemeId), legacyTheme.getId());
mongockTemplate.updateMulti(
new Query(where(fieldName(QApplication.application.deleted)).is(false)), update, Application.class
);
}
/**
* This method sets the key formData.aggregate.limit to 101 for all Mongo plugin actions.
* It iterates over each action id one by one to avoid out of memory error.
@ -4817,6 +4787,11 @@ public class DatabaseChangelog {
return true;
}
@ChangeSet(order = "108", id = "create-system-themes", author = "")
public void createSystemThemes(MongockTemplate mongockTemplate) throws IOException {
createSystemThemes2(mongockTemplate);
}
/**
* This migration adds a new field to Mongo aggregate command to set batchSize: formData.aggregate.limit. Its value
* is set by this migration to 101 for all existing actions since this is the default `batchSize` used by
@ -5025,4 +5000,75 @@ public class DatabaseChangelog {
);
}
/**
* Adding this migration again because we've added permission to themes.
* Also there are couple of changes in the system theme properties.
* @param mongockTemplate
* @throws IOException
*/
@ChangeSet(order = "117", id = "create-system-themes-v2", author = "")
public void createSystemThemes2(MongockTemplate mongockTemplate) throws IOException {
Index systemThemeIndex = new Index()
.on(fieldName(QTheme.theme.isSystemTheme), Sort.Direction.ASC)
.named("system_theme_index")
.background();
Index applicationIdIndex = new Index()
.on(fieldName(QTheme.theme.applicationId), Sort.Direction.ASC)
.on(fieldName(QTheme.theme.deleted), Sort.Direction.ASC)
.named("application_id_index")
.background();
dropIndexIfExists(mongockTemplate, Theme.class, "system_theme_index");
dropIndexIfExists(mongockTemplate, Theme.class, "application_id_index");
ensureIndexes(mongockTemplate, Theme.class, systemThemeIndex, applicationIdIndex);
final String themesJson = StreamUtils.copyToString(
new DefaultResourceLoader().getResource("system-themes.json").getInputStream(),
Charset.defaultCharset()
);
Theme[] themes = new Gson().fromJson(themesJson, Theme[].class);
Theme legacyTheme = null;
boolean themeExists = false;
Policy policyWithCurrentPermission = Policy.builder().permission(READ_THEMES.getValue())
.users(Set.of(FieldName.ANONYMOUS_USER)).build();
for (Theme theme : themes) {
theme.setSystemTheme(true);
theme.setCreatedAt(Instant.now());
theme.setPolicies(Set.of(policyWithCurrentPermission));
Query query = new Query(Criteria.where(fieldName(QTheme.theme.name)).is(theme.getName())
.and(fieldName(QTheme.theme.isSystemTheme)).is(true));
Theme savedTheme = mongockTemplate.findOne(query, Theme.class);
if(savedTheme == null) { // this theme does not exist, create it
savedTheme = mongockTemplate.save(theme);
} else { // theme already found, update
themeExists = true;
savedTheme.setPolicies(theme.getPolicies());
savedTheme.setConfig(theme.getConfig());
savedTheme.setProperties(theme.getProperties());
savedTheme.setStylesheet(theme.getStylesheet());
if(savedTheme.getCreatedAt() == null) {
savedTheme.setCreatedAt(Instant.now());
}
mongockTemplate.save(savedTheme);
}
if(theme.getName().equalsIgnoreCase(Theme.LEGACY_THEME_NAME)) {
legacyTheme = savedTheme;
}
}
if(!themeExists) { // this is the first time we're running the migration
// migrate all applications and set legacy theme to them in both mode
Update update = new Update().set(fieldName(QApplication.application.publishedModeThemeId), legacyTheme.getId())
.set(fieldName(QApplication.application.editModeThemeId), legacyTheme.getId());
mongockTemplate.updateMulti(
new Query(where(fieldName(QApplication.application.deleted)).is(false)), update, Application.class
);
}
}
}

View File

@ -58,13 +58,13 @@ public class CustomCommentThreadRepositoryCEImpl extends BaseAppsmithRepositoryI
where(fieldName(QCommentThread.commentThread.applicationId)).is(applicationId),
where(fieldName(QCommentThread.commentThread.isPrivate)).is(TRUE)
);
return queryOne(criteria, AclPermission.READ_THREAD);
return queryOne(criteria, AclPermission.READ_THREADS);
}
@Override
public Mono<UpdateResult> removeSubscriber(String threadId, String username) {
Update update = new Update().pull(fieldName(QCommentThread.commentThread.subscribers), username);
return this.updateById(threadId, update, AclPermission.READ_THREAD);
return this.updateById(threadId, update, AclPermission.READ_THREADS);
}
@Override
@ -92,7 +92,7 @@ public class CustomCommentThreadRepositoryCEImpl extends BaseAppsmithRepositoryI
where(fieldName(QCommentThread.commentThread.applicationId)).is(applicationId),
where(resolvedActiveFieldKey).is(false)
);
return count(criteriaList, AclPermission.READ_THREAD);
return count(criteriaList, AclPermission.READ_THREADS);
}
@Override

View File

@ -1,11 +1,13 @@
package com.appsmith.server.repositories.ce;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.repositories.AppsmithRepository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface CustomThemeRepositoryCE extends AppsmithRepository<Theme> {
Flux<Theme> getApplicationThemes(String applicationId, AclPermission aclPermission);
Flux<Theme> getSystemThemes();
Mono<Theme> getSystemThemeByName(String themeName);
}

View File

@ -1,5 +1,6 @@
package com.appsmith.server.repositories.ce;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.QTheme;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.repositories.BaseAppsmithRepositoryImpl;
@ -24,10 +25,18 @@ public class CustomThemeRepositoryCEImpl extends BaseAppsmithRepositoryImpl<Them
}
@Override
public Flux<Theme> getApplicationThemes(String applicationId, AclPermission aclPermission) {
Criteria appThemeCriteria = Criteria.where(fieldName(QTheme.theme.applicationId)).is(applicationId);
Criteria systemThemeCriteria = Criteria.where(fieldName(QTheme.theme.isSystemTheme)).is(Boolean.TRUE);
Criteria criteria = new Criteria().orOperator(appThemeCriteria, systemThemeCriteria);
return queryAll(List.of(criteria), aclPermission);
}
@Override
public Flux<Theme> getSystemThemes() {
Criteria criteria = Criteria.where(fieldName(QTheme.theme.isSystemTheme)).is(Boolean.TRUE);
return queryAll(List.of(criteria), null);
Criteria systemThemeCriteria = Criteria.where(fieldName(QTheme.theme.isSystemTheme)).is(Boolean.TRUE);
return queryAll(List.of(systemThemeCriteria), AclPermission.READ_THEMES);
}
@Override
@ -35,6 +44,6 @@ public class CustomThemeRepositoryCEImpl extends BaseAppsmithRepositoryImpl<Them
String findNameRegex = String.format("^%s$", Pattern.quote(themeName));
Criteria criteria = where(fieldName(QTheme.theme.name)).regex(findNameRegex, "i")
.and(fieldName(QTheme.theme.isSystemTheme)).is(true);
return queryOne(List.of(criteria), null);
return queryOne(List.of(criteria), AclPermission.READ_THEMES);
}
}

View File

@ -1,5 +1,6 @@
package com.appsmith.server.services;
import com.appsmith.server.acl.PolicyGenerator;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.ThemeRepository;
import com.appsmith.server.services.ce.ThemeServiceCEImpl;
@ -14,7 +15,7 @@ import javax.validation.Validator;
@Slf4j
@Service
public class ThemeServiceImpl extends ThemeServiceCEImpl implements ThemeService {
public ThemeServiceImpl(Scheduler scheduler, Validator validator, MongoConverter mongoConverter, ReactiveMongoTemplate reactiveMongoTemplate, ThemeRepository repository, AnalyticsService analyticsService, ApplicationRepository applicationRepository) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService, applicationRepository);
public ThemeServiceImpl(Scheduler scheduler, Validator validator, MongoConverter mongoConverter, ReactiveMongoTemplate reactiveMongoTemplate, ThemeRepository repository, AnalyticsService analyticsService, ApplicationRepository applicationRepository, PolicyGenerator policyGenerator) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService, applicationRepository, policyGenerator);
}
}

View File

@ -697,16 +697,6 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
application1.setModifiedBy(applicationUserTuple2.getT2().getUsername()); // setting modified by to current user
return applicationService.createDefault(application1);
})
// duplicate the source application's themes if required i.e. if they were customized
.flatMap(application ->
themeService.cloneThemeToApplication(sourceApplication.getEditModeThemeId(), application.getId())
.zipWith(themeService.cloneThemeToApplication(sourceApplication.getPublishedModeThemeId(), application.getId()))
.map(themesZip -> {
application.setEditModeThemeId(themesZip.getT1().getId());
application.setPublishedModeThemeId(themesZip.getT2().getId());
return application;
})
)
// Now fetch the pages of the source application, clone and add them to this new application
.flatMap(savedApplication -> Flux.fromIterable(sourceApplication.getPages())
.flatMap(applicationPage -> {
@ -728,6 +718,20 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
savedApplication.setPages(clonedPages);
return applicationService.save(savedApplication);
})
)
// duplicate the source application's themes if required i.e. if they were customized
.flatMap(application ->
themeService.cloneThemeToApplication(sourceApplication.getEditModeThemeId(), application)
.zipWith(themeService.cloneThemeToApplication(sourceApplication.getPublishedModeThemeId(), application))
.flatMap(themesZip -> {
String editModeThemeId = themesZip.getT1().getId();
String publishedModeThemeId = themesZip.getT2().getId();
application.setEditModeThemeId(editModeThemeId);
application.setPublishedModeThemeId(publishedModeThemeId);
return applicationService.setAppTheme(
application.getId(), editModeThemeId, publishedModeThemeId, MANAGE_APPLICATIONS
).thenReturn(application);
})
);
});
@ -842,9 +846,9 @@ public class ApplicationPageServiceCEImpl implements ApplicationPageServiceCE {
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId)))
.cache();
Mono<Theme> publishThemeMono = applicationMono.flatMap(application -> themeService.publishTheme(
application.getEditModeThemeId(), application.getPublishedModeThemeId(), application.getId()
));
Mono<Theme> publishThemeMono = applicationMono.flatMap(
application -> themeService.publishTheme(application.getId())
);
Flux<NewPage> publishApplicationAndPages = applicationMono
//Return all the pages in the Application

View File

@ -5,6 +5,7 @@ import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.GitAuth;
import com.appsmith.server.dtos.ApplicationAccessDTO;
import com.appsmith.server.services.CrudService;
import com.mongodb.client.result.UpdateResult;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@ -62,4 +63,6 @@ public interface ApplicationServiceCE extends CrudService<Application, String> {
String getRandomAppCardColor();
Mono<UpdateResult> setAppTheme(String applicationId, String editModeThemeId, String publishedModeThemeId, AclPermission aclPermission);
}

View File

@ -13,6 +13,7 @@ import com.appsmith.server.domains.GitAuth;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Page;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.ApplicationAccessDTO;
@ -28,6 +29,7 @@ import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.BaseService;
import com.appsmith.server.services.ConfigService;
import com.appsmith.server.services.SessionUserService;
import com.mongodb.client.result.UpdateResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DuplicateKeyException;
@ -308,18 +310,23 @@ public class ApplicationServiceCEImpl extends BaseService<ApplicationRepository,
Map<String, Policy> pagePolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(applicationPolicyMap, Application.class, Page.class);
Map<String, Policy> actionPolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(pagePolicyMap, Page.class, Action.class);
Map<String, Policy> datasourcePolicyMap = policyUtils.generatePolicyFromPermission(Set.of(EXECUTE_DATASOURCES), user);
Map<String, Policy> themePolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(
applicationPolicyMap, Application.class, Theme.class
);
final Flux<NewPage> updatedPagesFlux = policyUtils
.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, isPublic);
// Use the same policy map as actions for action collections since action collections have the same kind of permissions
final Flux<ActionCollection> updatedActionCollectionsFlux = policyUtils
.updateWithPagePermissionsToAllItsActionCollections(application.getId(), actionPolicyMap, isPublic);
Flux<Theme> updatedThemesFlux = policyUtils.updateThemePolicies(application, themePolicyMap, isPublic);
final Flux<NewAction> updatedActionsFlux = updatedPagesFlux
.collectList()
.thenMany(updatedActionCollectionsFlux)
.collectList()
.then(Mono.justOrEmpty(application.getId()))
.thenMany(updatedThemesFlux)
.collectList()
.flatMapMany(applicationId -> policyUtils.updateWithPagePermissionsToAllItsActions(application.getId(), actionPolicyMap, isPublic));
return updatedActionsFlux
@ -547,4 +554,8 @@ public class ApplicationServiceCEImpl extends BaseService<ApplicationRepository,
return ApplicationConstants.APP_CARD_COLORS[randomColorIndex];
}
@Override
public Mono<UpdateResult> setAppTheme(String applicationId, String editModeThemeId, String publishedModeThemeId, AclPermission aclPermission) {
return repository.setAppTheme(applicationId, editModeThemeId, publishedModeThemeId, aclPermission);
}
}

View File

@ -61,9 +61,9 @@ import static com.appsmith.server.acl.AclPermission.COMMENT_ON_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.READ_COMMENT;
import static com.appsmith.server.acl.AclPermission.READ_COMMENTS;
import static com.appsmith.server.acl.AclPermission.READ_PAGES;
import static com.appsmith.server.acl.AclPermission.READ_THREAD;
import static com.appsmith.server.acl.AclPermission.READ_THREADS;
import static com.appsmith.server.constants.CommentConstants.APPSMITH_BOT_NAME;
import static com.appsmith.server.constants.CommentConstants.APPSMITH_BOT_USERNAME;
import static java.lang.Boolean.FALSE;
@ -177,7 +177,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
comment.setPageId(branchedPageId);
comment.setApplicationId(branchedApplicationId);
return threadRepository
.findById(threadId, AclPermission.COMMENT_ON_THREAD)
.findById(threadId, AclPermission.COMMENT_ON_THREADS)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.COMMENT_THREAD, threadId)))
.flatMap(commentThread -> updateThreadOnAddComment(commentThread, comment, user))
.flatMap(commentThread -> create(commentThread, user, comment, originHeader));
@ -188,7 +188,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
@Override
public Mono<Comment> findByIdAndBranchName(String id, String branchName) {
// Ignore branch name as comments are not shared across git branches
return repository.findById(id, READ_COMMENT)
return repository.findById(id, READ_COMMENTS)
.map(responseUtils::updatePageAndAppIdWithDefaultResourcesForComments);
}
@ -234,9 +234,9 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
CommentThread.class
));
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD),
Set.of(AclPermission.MANAGE_THREADS),
commentThread.getAuthorUsername()
).get(AclPermission.MANAGE_THREAD.getValue()));
).get(AclPermission.MANAGE_THREADS.getValue()));
commentThread.setPolicies(policies);
return commentThread;
});
@ -266,9 +266,9 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
Comment.class
);
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_COMMENT),
Set.of(AclPermission.MANAGE_COMMENTS),
user
).get(AclPermission.MANAGE_COMMENT.getValue()));
).get(AclPermission.MANAGE_COMMENTS.getValue()));
comment.setPolicies(policies);
Mono<Comment> commentMono;
@ -429,7 +429,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
// Comments and threads can't be moved between the pages and applications.
comment.setApplicationId(null);
comment.setPageId(null);
return repository.updateById(id, comment, AclPermission.MANAGE_COMMENT)
return repository.updateById(id, comment, AclPermission.MANAGE_COMMENTS)
.flatMap(analyticsService::sendUpdateEvent)
.map(responseUtils::updatePageAndAppIdWithDefaultResourcesForComments);
}
@ -447,7 +447,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
} else {
return Mono.just(user);
}
}).zipWith(threadRepository.findById(threadId, AclPermission.READ_THREAD))
}).zipWith(threadRepository.findById(threadId, AclPermission.READ_THREADS))
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, "comment thread", threadId))
)
@ -489,7 +489,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
}
return threadRepository
.updateById(threadId, commentThread, AclPermission.READ_THREAD)
.updateById(threadId, commentThread, AclPermission.READ_THREADS)
.flatMap(updatedThread -> {
updatedThread.setIsViewed(true);
// Update branched applicationId and pageId with default Ids
@ -520,7 +520,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
public Flux<Comment> get(MultiValueMap<String, String> params) {
// Remove branch name as comments are not shared across branches
params.remove(FieldName.DEFAULT_RESOURCES + "." + FieldName.BRANCH_NAME);
return super.getWithPermission(params, READ_COMMENT)
return super.getWithPermission(params, READ_COMMENTS)
.map(responseUtils::updatePageAndAppIdWithDefaultResourcesForComments);
}
@ -580,7 +580,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
// user is app viewer, show only PUBLISHED comment threads
commentThreadFilterDTO.setMode(ApplicationMode.PUBLISHED);
}
return threadRepository.find(commentThreadFilterDTO, AclPermission.READ_THREAD)
return threadRepository.find(commentThreadFilterDTO, AclPermission.READ_THREADS)
.collectList()
.flatMap(threads -> {
final Map<String, CommentThread> threadsByThreadId = new HashMap<>();
@ -626,10 +626,10 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
*/
@Override
public Mono<Comment> deleteComment(String id) {
return repository.findById(id, AclPermission.MANAGE_COMMENT)
return repository.findById(id, AclPermission.MANAGE_COMMENTS)
.switchIfEmpty(Mono.error(new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.COMMENT, id)))
.flatMap(repository::archive)
.flatMap(comment -> threadRepository.findById(comment.getThreadId(), READ_THREAD).flatMap(commentThread ->
.flatMap(comment -> threadRepository.findById(comment.getThreadId(), READ_THREADS).flatMap(commentThread ->
sendCommentNotifications(commentThread.getSubscribers(), comment, CommentNotificationEvent.DELETED)
.thenReturn(comment)
))
@ -639,7 +639,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
@Override
public Mono<CommentThread> deleteThread(String threadId) {
return threadRepository.findById(threadId, AclPermission.MANAGE_THREAD)
return threadRepository.findById(threadId, AclPermission.MANAGE_THREADS)
.flatMap(threadRepository::archive)
.flatMap(commentThread ->
notificationService.createNotification(
@ -653,7 +653,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
@Override
public Mono<Boolean> createReaction(String commentId, Comment.Reaction reaction) {
return Mono.zip(
repository.findById(commentId, READ_COMMENT),
repository.findById(commentId, READ_COMMENTS),
sessionUserService.getCurrentUser()
)
.flatMap(tuple -> {
@ -671,7 +671,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
@Override
public Mono<Boolean> deleteReaction(String commentId, Comment.Reaction reaction) {
return Mono.zip(
repository.findById(commentId, READ_COMMENT),
repository.findById(commentId, READ_COMMENTS),
sessionUserService.getCurrentUser()
)
.flatMap(tuple -> {
@ -702,7 +702,7 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
Mono<Long> commentSeq;
if (TRUE.equals(commentThread.getIsPrivate())) {
Collection<Policy> policyCollection = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD, AclPermission.COMMENT_ON_THREAD),
Set.of(AclPermission.MANAGE_THREADS, AclPermission.COMMENT_ON_THREADS),
user
).values();
policies.addAll(policyCollection);
@ -714,9 +714,9 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
CommentThread.class
));
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD),
Set.of(AclPermission.MANAGE_THREADS),
user
).get(AclPermission.MANAGE_THREAD.getValue()));
).get(AclPermission.MANAGE_THREADS.getValue()));
commentSeq = sequenceService.getNext(CommentThread.class, application.getId());
}
commentThread.setPolicies(policies);
@ -740,9 +740,9 @@ public class CommentServiceCEImpl extends BaseService<CommentRepository, Comment
Comment.class
);
policies.add(policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_COMMENT),
Set.of(AclPermission.MANAGE_COMMENTS),
user
).get(AclPermission.MANAGE_COMMENT.getValue()));
).get(AclPermission.MANAGE_COMMENTS.getValue()));
comment.setPolicies(policies);
Comment.Block block = new Comment.Block();

View File

@ -1,12 +1,18 @@
package com.appsmith.server.services.ce;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.services.CrudService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface ThemeServiceCE extends CrudService<Theme, String> {
Mono<Theme> getApplicationTheme(String applicationId, ApplicationMode applicationMode);
Flux<Theme> getApplicationThemes(String applicationId);
Flux<Theme> getSystemThemes();
Mono<Theme> getSystemTheme(String themeName);
Mono<Theme> updateTheme(String applicationId, Theme resource);
Mono<Theme> changeCurrentTheme(String themeId, String applicationId);
@ -18,14 +24,17 @@ public interface ThemeServiceCE extends CrudService<Theme, String> {
Mono<String> getDefaultThemeId();
/**
* Duplicates a theme if the theme is customized one. It'll set the application id to the new theme.
* Duplicates a theme if the theme is customized one.
* If the source theme is a system theme, it'll skip creating a new theme and return the system theme instead.
* @param srcThemeId ID of source theme that needs to be duplicated
* @param destApplicationId ID of the application for which theme'll be created
* @return newly created theme if source is not system theme, otherwise return the system theme
*/
Mono<Theme> cloneThemeToApplication(String srcThemeId, String destApplicationId);
Mono<Theme> publishTheme(String editModeThemeId, String publishedThemeId, String applicationId);
void resetDefaultThemeIdCache();
Mono<Theme> cloneThemeToApplication(String srcThemeId, Application destApplication);
Mono<Theme> publishTheme(String applicationId);
Mono<Theme> persistCurrentTheme(String applicationId, Theme theme);
Mono<Theme> getThemeById(String themeId, AclPermission permission);
Mono<Theme> save(Theme theme);
Mono<Theme> updateName(String id, Theme theme);
Mono<Theme> getOrSaveTheme(Theme theme, Application destApplication);
}

View File

@ -1,7 +1,9 @@
package com.appsmith.server.services.ce;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.acl.PolicyGenerator;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.exceptions.AppsmithError;
@ -19,43 +21,48 @@ import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.util.function.Tuples;
import javax.validation.Validator;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_THEMES;
import static com.appsmith.server.acl.AclPermission.READ_THEMES;
@Slf4j
public class ThemeServiceCEImpl extends BaseService<ThemeRepositoryCE, Theme, String> implements ThemeServiceCE {
private final ApplicationRepository applicationRepository;
private final PolicyGenerator policyGenerator;
private String defaultThemeId; // acts as a simple cache so that we don't need to fetch from DB always
public ThemeServiceCEImpl(Scheduler scheduler, Validator validator, MongoConverter mongoConverter, ReactiveMongoTemplate reactiveMongoTemplate, ThemeRepository repository, AnalyticsService analyticsService, ApplicationRepository applicationRepository) {
public ThemeServiceCEImpl(Scheduler scheduler, Validator validator, MongoConverter mongoConverter, ReactiveMongoTemplate reactiveMongoTemplate, ThemeRepository repository, AnalyticsService analyticsService, ApplicationRepository applicationRepository, PolicyGenerator policyGenerator) {
super(scheduler, validator, mongoConverter, reactiveMongoTemplate, repository, analyticsService);
this.applicationRepository = applicationRepository;
}
@Override
public Flux<Theme> get(MultiValueMap<String, String> params) {
return repository.getSystemThemes(); // return the list of system themes
this.policyGenerator = policyGenerator;
}
@Override
public Mono<Theme> create(Theme resource) {
// user can get the list of themes under an application only
throw new UnsupportedOperationException();
return repository.save(resource);
}
@Override
public Mono<Theme> update(String s, Theme resource) {
// we don't allow to update a theme by id, user can only update a theme under their application
throw new UnsupportedOperationException();
throw new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION);
}
@Override
public Mono<Theme> getById(String s) {
// TODO: better to add permission check
return repository.findById(s);
// we don't allow to get a theme by id from DB
throw new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION);
}
@Override
public Flux<Theme> get(MultiValueMap<String, String> params) {
// we return all system themes
return repository.getSystemThemes();
}
@Override
@ -69,45 +76,75 @@ public class ThemeServiceCEImpl extends BaseService<ThemeRepositoryCE, Theme, St
if(applicationMode == ApplicationMode.PUBLISHED) {
themeId = application.getPublishedModeThemeId();
}
if(!StringUtils.isEmpty(themeId)) {
return repository.findById(themeId);
if(StringUtils.hasLength(themeId)) {
return repository.findById(themeId, READ_THEMES);
} else { // theme id is not present, return default theme
return repository.getSystemThemeByName(Theme.DEFAULT_THEME_NAME);
}
});
}
@Override
public Flux<Theme> getApplicationThemes(String applicationId) {
return repository.getApplicationThemes(applicationId, READ_THEMES);
}
@Override
public Flux<Theme> getSystemThemes() {
return repository.getSystemThemes();
}
@Override
public Mono<Theme> updateTheme(String applicationId, Theme resource) {
return applicationRepository.findById(applicationId, AclPermission.MANAGE_APPLICATIONS)
.flatMap(application -> {
// makes sure user has permission to edit application and an application exists by this applicationId
// check if this application has already a customized them
return saveThemeForApplication(application.getEditModeThemeId(), resource, applicationId, ApplicationMode.EDIT);
return saveThemeForApplication(application.getEditModeThemeId(), resource, application, ApplicationMode.EDIT);
});
}
@Override
public Mono<Theme> changeCurrentTheme(String newThemeId, String applicationId) {
// set provided theme to application and return that theme object
Mono<Theme> setAppThemeMono = applicationRepository.setAppTheme(
applicationId, newThemeId,null, MANAGE_APPLICATIONS
).then(repository.findById(newThemeId));
// in case a customized theme was set to application, we need to delete that
return applicationRepository.findById(applicationId, AclPermission.MANAGE_APPLICATIONS)
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId))
)
.flatMap(application -> repository.findById(application.getEditModeThemeId())
.flatMap(application -> repository.findById(application.getEditModeThemeId(), READ_THEMES)
.defaultIfEmpty(new Theme())
.flatMap(currentTheme -> {
if (!StringUtils.isEmpty(currentTheme.getId()) && !currentTheme.isSystemTheme()) {
// current theme is not a system theme but customized one, delete this
return repository.delete(currentTheme).then(setAppThemeMono);
.zipWith(repository.findById(newThemeId, READ_THEMES))
.flatMap(themeTuple2 -> {
Theme currentTheme = themeTuple2.getT1();
Theme newTheme = themeTuple2.getT2();
Mono<Theme> saveThemeMono;
if(!newTheme.isSystemTheme()) {
// we'll create a copy of newTheme
newTheme.setId(null);
newTheme.setApplicationId(null);
newTheme.setOrganizationId(null);
newTheme.setPolicies(policyGenerator.getAllChildPolicies(
application.getPolicies(), Application.class, Theme.class
));
saveThemeMono = repository.save(newTheme);
} else {
saveThemeMono = Mono.just(newTheme);
}
return setAppThemeMono;
}));
return saveThemeMono.flatMap(savedTheme -> {
if (StringUtils.hasLength(currentTheme.getId()) && !currentTheme.isSystemTheme()
&& !StringUtils.hasLength(currentTheme.getApplicationId())) {
// current theme is neither a system theme nor app theme, delete the user customizations
return repository.delete(currentTheme).then(applicationRepository.setAppTheme(
applicationId, savedTheme.getId(),null, MANAGE_APPLICATIONS
)).thenReturn(savedTheme);
} else {
return applicationRepository.setAppTheme(
applicationId, savedTheme.getId(),null, MANAGE_APPLICATIONS
).thenReturn(savedTheme);
}
});
})
);
}
@Override
@ -122,79 +159,97 @@ public class ThemeServiceCEImpl extends BaseService<ThemeRepositoryCE, Theme, St
}
@Override
public Mono<Theme> cloneThemeToApplication(String srcThemeId, String destApplicationId) {
return applicationRepository.findById(destApplicationId, MANAGE_APPLICATIONS).then(
// make sure the current user has permission to manage application
repository.findById(srcThemeId).flatMap(theme -> {
if (theme.isSystemTheme()) { // it's a system theme, no need to copy
return Mono.just(theme);
} else { // it's a customized theme, create a copy and return the copy
theme.setId(null); // setting id to null so that save method will create a new instance
return repository.save(theme);
}
})
);
}
@Override
public Mono<Theme> publishTheme(String editModeThemeId, String publishedThemeId, String applicationId) {
Mono<Theme> editModeThemeMono;
if(!StringUtils.hasLength(editModeThemeId)) { // theme id is empty, use the default theme
editModeThemeMono = repository.getSystemThemeByName(Theme.LEGACY_THEME_NAME);
} else { // theme id is not empty, fetch it by id
editModeThemeMono = repository.findById(editModeThemeId);
}
Mono<Theme> publishThemeMono = editModeThemeMono.flatMap(editModeTheme -> {
if (editModeTheme.isSystemTheme()) { // system theme is set as edit mode theme
// just set the system theme id as edit and published mode theme id to application object
return applicationRepository.setAppTheme(
applicationId, editModeTheme.getId(), editModeTheme.getId(), MANAGE_APPLICATIONS
).thenReturn(editModeTheme);
} else { // a customized theme is set as edit mode theme, copy that theme for published mode
return saveThemeForApplication(publishedThemeId, editModeTheme, applicationId, ApplicationMode.PUBLISHED);
public Mono<Theme> cloneThemeToApplication(String srcThemeId, Application destApplication) {
return repository.findById(srcThemeId, READ_THEMES).flatMap(theme -> {
if (theme.isSystemTheme()) { // it's a system theme, no need to copy
return Mono.just(theme);
} else { // it's a customized theme, create a copy and return the copy
theme.setId(null); // setting id to null so that save method will create a new instance
theme.setApplicationId(null);
theme.setOrganizationId(null);
theme.setPolicies(policyGenerator.getAllChildPolicies(
destApplication.getPolicies(), Application.class, Theme.class
));
return repository.save(theme);
}
});
}
/**
* Publishes a theme from edit mode to published mode
* @param applicationId application id
* @return Mono of theme object that was set in published mode
*/
@Override
public Mono<Theme> publishTheme(String applicationId) {
// fetch application to make sure user has permission to manage this application
return applicationRepository.findById(applicationId, MANAGE_APPLICATIONS).then(publishThemeMono);
return applicationRepository.findById(applicationId, MANAGE_APPLICATIONS).flatMap(application -> {
Mono<Theme> editModeThemeMono;
if(!StringUtils.hasLength(application.getEditModeThemeId())) { // theme id is empty, use the default theme
editModeThemeMono = repository.getSystemThemeByName(Theme.LEGACY_THEME_NAME);
} else { // theme id is not empty, fetch it by id
editModeThemeMono = repository.findById(application.getEditModeThemeId(), READ_THEMES);
}
return editModeThemeMono.flatMap(editModeTheme -> {
if (editModeTheme.isSystemTheme()) { // system theme is set as edit mode theme
// Delete published mode theme if it was a copy of custom theme
return deletePublishedCustomizedThemeCopy(application.getPublishedModeThemeId()).then(
// Set the system theme id as edit and published mode theme id to application object
applicationRepository.setAppTheme(
applicationId, editModeTheme.getId(), editModeTheme.getId(), MANAGE_APPLICATIONS
)
).thenReturn(editModeTheme);
} else { // a customized theme is set as edit mode theme, copy that theme for published mode
return saveThemeForApplication(
application.getPublishedModeThemeId(), editModeTheme, application, ApplicationMode.PUBLISHED
);
}
});
});
}
/**
* Creates a new theme if Theme with provided themeId is a system theme.
* It sets the properties from the provided theme resource to the existing or newly created theme.
* It'll also update the application if a new theme was created.
* @param themeId ID of the existing theme that might be updated
* @param resource new theme DTO that'll be stored as a new theme or override the existing theme
* @param applicationId Application that contains the theme
* @param currentThemeId ID of the existing theme that might be updated
* @param targetThemeResource new theme DTO that'll be stored as a new theme or override the existing theme
* @param application Application that contains the theme
* @param applicationMode In which mode this theme will be set
* @return Updated or newly created theme Publisher
*/
private Mono<Theme> saveThemeForApplication(String themeId, Theme resource, String applicationId, ApplicationMode applicationMode) {
return repository.findById(themeId)
.flatMap(theme -> {
private Mono<Theme> saveThemeForApplication(String currentThemeId, Theme targetThemeResource, Application application, ApplicationMode applicationMode) {
return repository.findById(currentThemeId, READ_THEMES)
.flatMap(currentTheme -> {
// set the edit mode values to published mode theme
theme.setConfig(resource.getConfig());
theme.setStylesheet(resource.getStylesheet());
theme.setProperties(resource.getProperties());
theme.setName(resource.getName());
currentTheme.setConfig(targetThemeResource.getConfig());
currentTheme.setStylesheet(targetThemeResource.getStylesheet());
currentTheme.setProperties(targetThemeResource.getProperties());
if(StringUtils.hasLength(targetThemeResource.getName())) {
currentTheme.setName(targetThemeResource.getName());
}
boolean newThemeCreated = false;
if (theme.isSystemTheme()) {
if (currentTheme.isSystemTheme()) {
// if this is a system theme, create a new one
theme.setId(null); // setting id to null will create a new theme
theme.setSystemTheme(false);
currentTheme.setId(null); // setting id to null will create a new theme
currentTheme.setSystemTheme(false);
currentTheme.setPolicies(policyGenerator.getAllChildPolicies(
application.getPolicies(), Application.class, Theme.class
));
newThemeCreated = true;
}
return repository.save(theme).zipWith(Mono.just(newThemeCreated));
return repository.save(currentTheme).zipWith(Mono.just(newThemeCreated));
}).flatMap(savedThemeTuple -> {
Theme theme = savedThemeTuple.getT1();
if (savedThemeTuple.getT2()) { // new published theme created, update the application
if (savedThemeTuple.getT2()) { // new theme created, update the application
if(applicationMode == ApplicationMode.EDIT) {
return applicationRepository.setAppTheme(
applicationId, theme.getId(), null, MANAGE_APPLICATIONS
application.getId(), theme.getId(), null, MANAGE_APPLICATIONS
).thenReturn(theme);
} else {
return applicationRepository.setAppTheme(
applicationId, null, theme.getId(), MANAGE_APPLICATIONS
application.getId(), null, theme.getId(), MANAGE_APPLICATIONS
).thenReturn(theme);
}
} else {
@ -204,7 +259,117 @@ public class ThemeServiceCEImpl extends BaseService<ThemeRepositoryCE, Theme, St
}
@Override
public void resetDefaultThemeIdCache() {
defaultThemeId = null;
public Mono<Theme> persistCurrentTheme(String applicationId, Theme resource) {
return applicationRepository.findById(applicationId, MANAGE_APPLICATIONS)
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, applicationId))
)
.flatMap(application -> {
String themeId = application.getEditModeThemeId();
if(!StringUtils.hasLength(themeId)) { // theme id is not present, raise error
return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION));
} else {
return repository.findById(themeId, READ_THEMES)
.map(theme -> Tuples.of(theme, application));
}
})
.flatMap(themeAndApplicationTuple -> {
Theme theme = themeAndApplicationTuple.getT1();
Application application = themeAndApplicationTuple.getT2();
theme.setId(null); // we'll create a copy so setting id to null
theme.setSystemTheme(false);
theme.setApplicationId(applicationId);
theme.setOrganizationId(application.getOrganizationId());
theme.setPolicies(policyGenerator.getAllChildPolicies(
application.getPolicies(), Application.class, Theme.class
));
if(StringUtils.hasLength(resource.getName())) {
theme.setName(resource.getName());
} else {
theme.setName(theme.getName() + " copy");
}
return repository.save(theme);
});
}
/**
* This method will fetch a theme by id and delete this if it's not a system theme.
* When an app is published with a customized theme, we store a copy of that theme so that changes are available
* in published mode even user has changed the theme in edit mode. When user switches back to another theme and
* publish the application where that app was previously published with a custom theme, we should delete that copy.
* Otherwise there'll be a lot of orphan theme copies that were set a published mode once but are used no more.
* @param themeId id of the theme that'll be deleted
* @return deleted theme mono
*/
private Mono<Theme> deletePublishedCustomizedThemeCopy(String themeId) {
if(!StringUtils.hasLength(themeId)) {
return Mono.empty();
}
return repository.findById(themeId).flatMap(theme -> {
if(!theme.isSystemTheme()) {
return repository.deleteById(themeId).thenReturn(theme);
}
return Mono.just(theme);
});
}
@Override
public Mono<Theme> delete(String themeId) {
return repository.findById(themeId, MANAGE_THEMES)
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.APPLICATION, FieldName.THEME))
).flatMap(theme -> {
if (StringUtils.hasLength(theme.getApplicationId())) { // only persisted themes are allowed to delete
return repository.archive(theme);
} else {
return Mono.error(new AppsmithException(AppsmithError.UNSUPPORTED_OPERATION));
}
});
}
@Override
public Mono<Theme> getSystemTheme(String themeName) {
return repository.getSystemThemeByName(themeName);
}
@Override
public Mono<Theme> getThemeById(String themeId, AclPermission permission) {
return repository.findById(themeId, permission);
}
@Override
public Mono<Theme> save(Theme theme) {
return repository.save(theme);
}
@Override
public Mono<Theme> updateName(String id, Theme themeDto) {
return repository.findById(id, MANAGE_THEMES)
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.NO_RESOURCE_FOUND, FieldName.THEME, id))
).flatMap(theme -> {
if(StringUtils.hasLength(themeDto.getName())) {
theme.setName(themeDto.getName());
}
return repository.save(theme);
});
}
@Override
public Mono<Theme> getOrSaveTheme(Theme theme, Application destApplication) {
if(theme == null) { // this application was exported without theme, assign the legacy theme to it
return repository.getSystemThemeByName(Theme.LEGACY_THEME_NAME); // return the default theme
} else if (theme.isSystemTheme()) {
return repository.getSystemThemeByName(theme.getName());
} else {
theme.setApplicationId(null);
theme.setOrganizationId(null);
theme.setPolicies(policyGenerator.getAllChildPolicies(
destApplication.getPolicies(), Application.class, Theme.class
));
return repository.save(theme);
}
}
}

View File

@ -13,6 +13,7 @@ import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Page;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.UserRole;
import com.appsmith.server.exceptions.AppsmithError;
@ -172,6 +173,9 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
Map<String, Policy> commentThreadPolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(
applicationPolicyMap, Application.class, CommentThread.class
);
Map<String, Policy> themePolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(
applicationPolicyMap, Application.class, Theme.class
);
//Now update the organization policies
Organization updatedOrganization = policyUtils.addPoliciesToExistingObject(orgPolicyMap, organization);
updatedOrganization.setUserRoles(userRoles);
@ -179,7 +183,7 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
// Update the underlying application/page/action
Flux<Datasource> updatedDatasourcesFlux = policyUtils.updateWithNewPoliciesToDatasourcesByOrgId(updatedOrganization.getId(), datasourcePolicyMap, true);
Flux<Application> updatedApplicationsFlux = policyUtils.updateWithNewPoliciesToApplicationsByOrgId(updatedOrganization.getId(), applicationPolicyMap, true)
.cache();
.cache(); // .cache is very important, as we will execute once and reuse the results multiple times
Flux<NewPage> updatedPagesFlux = updatedApplicationsFlux
.flatMap(application -> policyUtils.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, true));
Flux<NewAction> updatedActionsFlux = updatedApplicationsFlux
@ -188,14 +192,18 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
.flatMap(application -> policyUtils.updateWithPagePermissionsToAllItsActionCollections(application.getId(), actionPolicyMap, true));
Flux<CommentThread> updatedThreadsFlux = updatedApplicationsFlux
.flatMap(application -> policyUtils.updateCommentThreadPermissions(application.getId(), commentThreadPolicyMap, user.getUsername(), true));
Flux<Theme> updatedThemesFlux = updatedApplicationsFlux
.flatMap(application -> policyUtils.updateThemePolicies(
application, themePolicyMap, true
));
return Mono.zip(
updatedDatasourcesFlux.collectList(),
updatedPagesFlux.collectList(),
updatedActionsFlux.collectList(),
updatedActionCollectionsFlux.collectList(),
Mono.just(updatedOrganization),
updatedThreadsFlux.collectList()
updatedThreadsFlux.collectList(),
updatedThemesFlux.collectList()
)
.flatMap(tuple -> {
//By now all the datasources/applications/pages/actions have been updated. Just save the organization now
@ -256,6 +264,9 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
Map<String, Policy> commentThreadPolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(
applicationPolicyMap, Application.class, CommentThread.class
);
Map<String, Policy> themePolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(
applicationPolicyMap, Application.class, Theme.class
);
//Now update the organization policies
Organization updatedOrganization = policyUtils.removePoliciesFromExistingObject(orgPolicyMap, organization);
@ -264,7 +275,7 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
// Update the underlying application/page/action
Flux<Datasource> updatedDatasourcesFlux = policyUtils.updateWithNewPoliciesToDatasourcesByOrgId(updatedOrganization.getId(), datasourcePolicyMap, false);
Flux<Application> updatedApplicationsFlux = policyUtils.updateWithNewPoliciesToApplicationsByOrgId(updatedOrganization.getId(), applicationPolicyMap, false)
.cache();
.cache(); // .cache is very important, as we will execute once and reuse the results multiple times
Flux<NewPage> updatedPagesFlux = updatedApplicationsFlux
.flatMap(application -> policyUtils.updateWithApplicationPermissionsToAllItsPages(application.getId(), pagePolicyMap, false));
Flux<NewAction> updatedActionsFlux = updatedApplicationsFlux
@ -275,6 +286,10 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
.flatMap(application -> policyUtils.updateCommentThreadPermissions(
application.getId(), commentThreadPolicyMap, user.getUsername(), false
));
Flux<Theme> updatedThemesFlux = updatedApplicationsFlux
.flatMap(application -> policyUtils.updateThemePolicies(
application, themePolicyMap, false
));
return Mono.zip(
updatedDatasourcesFlux.collectList(),
@ -282,7 +297,8 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
updatedActionsFlux.collectList(),
updatedActionCollectionsFlux.collectList(),
updatedThreadsFlux.collectList(),
Mono.just(updatedOrganization)
Mono.just(updatedOrganization),
updatedThemesFlux.collectList()
).flatMap(tuple -> {
//By now all the datasources/applications/pages/actions have been updated. Just save the organization now
Organization updatedOrgBeforeSave = tuple.getT6();
@ -435,6 +451,9 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
applicationPolicyMap, Application.class, CommentThread.class
);
Map<String, Policy> actionPolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(pagePolicyMap, Page.class, Action.class);
Map<String, Policy> themePolicyMap = policyUtils.generateInheritedPoliciesFromSourcePolicies(
applicationPolicyMap, Application.class, Theme.class
);
// Now update the organization policies
Organization updatedOrganization = policyUtils.addPoliciesToExistingObject(orgPolicyMap, organization);
@ -454,13 +473,19 @@ public class UserOrganizationServiceCEImpl implements UserOrganizationServiceCE
.flatMap(application -> policyUtils.updateCommentThreadPermissions(
application.getId(), commentThreadPolicyMap, null, true
));
Flux<Theme> updatedThemesFlux = updatedApplicationsFlux
.flatMap(application -> policyUtils.updateThemePolicies(
application, themePolicyMap, true
));
return Mono.when(
updatedDatasourcesFlux.collectList(),
updatedPagesFlux.collectList(),
updatedActionsFlux.collectList(),
updatedActionCollectionsFlux.collectList(),
updatedThreadsFlux.collectList())
updatedDatasourcesFlux.collectList(),
updatedPagesFlux.collectList(),
updatedActionsFlux.collectList(),
updatedActionCollectionsFlux.collectList(),
updatedThreadsFlux.collectList(),
updatedThemesFlux.collectList()
)
// By now all the
// data sources/applications/pages/actions/action collections/comment threads
// have been updated. Just save the organization now

View File

@ -13,6 +13,7 @@ import com.appsmith.server.services.LayoutCollectionService;
import com.appsmith.server.services.NewActionService;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.ThemeService;
import com.appsmith.server.services.UserService;
import com.appsmith.server.solutions.ce.ExamplesOrganizationClonerCEImpl;
import lombok.extern.slf4j.Slf4j;
@ -35,10 +36,11 @@ public class ExamplesOrganizationClonerImpl extends ExamplesOrganizationClonerCE
NewActionService newActionService,
LayoutActionService layoutActionService,
ActionCollectionService actionCollectionService,
LayoutCollectionService layoutCollectionService) {
LayoutCollectionService layoutCollectionService,
ThemeService themeService) {
super(organizationService, organizationRepository, datasourceService, datasourceRepository, configService,
sessionUserService, userService, applicationService, applicationPageService, newPageRepository,
newActionService, layoutActionService, actionCollectionService, layoutCollectionService);
newActionService, layoutActionService, actionCollectionService, layoutCollectionService, themeService);
}
}

View File

@ -5,7 +5,6 @@ import com.appsmith.server.repositories.DatasourceRepository;
import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.repositories.ThemeRepository;
import com.appsmith.server.services.ActionCollectionService;
import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.ApplicationService;
@ -15,6 +14,7 @@ import com.appsmith.server.services.NewPageService;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.SequenceService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.ThemeService;
import com.appsmith.server.solutions.ce.ImportExportApplicationServiceCEImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@ -38,11 +38,11 @@ public class ImportExportApplicationServiceImpl extends ImportExportApplicationS
ExamplesOrganizationCloner examplesOrganizationCloner,
ActionCollectionRepository actionCollectionRepository,
ActionCollectionService actionCollectionService,
ThemeRepository themeRepository) {
ThemeService themeService) {
super(datasourceService, sessionUserService, newActionRepository, datasourceRepository, pluginRepository,
organizationService, applicationService, newPageService, applicationPageService, newPageRepository,
newActionService, sequenceService, examplesOrganizationCloner, actionCollectionRepository,
actionCollectionService, themeRepository);
actionCollectionService, themeService);
}
}

View File

@ -6,6 +6,7 @@ import com.appsmith.external.models.AuthenticationDTO;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.DefaultResources;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application;
@ -13,6 +14,7 @@ import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ActionCollectionDTO;
import com.appsmith.server.dtos.ActionDTO;
@ -33,7 +35,9 @@ import com.appsmith.server.services.LayoutCollectionService;
import com.appsmith.server.services.NewActionService;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.ThemeService;
import com.appsmith.server.services.UserService;
import com.mongodb.client.result.UpdateResult;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
@ -68,6 +72,7 @@ public class ExamplesOrganizationClonerCEImpl implements ExamplesOrganizationClo
private final LayoutActionService layoutActionService;
private final ActionCollectionService actionCollectionService;
private final LayoutCollectionService layoutCollectionService;
private final ThemeService themeService;
public Mono<Organization> cloneExamplesOrganization() {
return sessionUserService
@ -417,17 +422,35 @@ public class ExamplesOrganizationClonerCEImpl implements ExamplesOrganizationClo
.flatMapMany(
savedApplication -> {
applicationIds.add(savedApplication.getId());
return newPageRepository
.findByApplicationId(templateApplicationId)
.map(newPage -> {
log.info("Preparing page for cloning {} {}.", newPage.getUnpublishedPage().getName(), newPage.getId());
newPage.setApplicationId(savedApplication.getId());
return newPage;
});
return forkThemes(application, savedApplication).thenMany(
newPageRepository
.findByApplicationId(templateApplicationId)
.map(newPage -> {
log.info("Preparing page for cloning {} {}.", newPage.getUnpublishedPage().getName(), newPage.getId());
newPage.setApplicationId(savedApplication.getId());
return newPage;
})
);
}
);
}
private Mono<UpdateResult> forkThemes(Application srcApplication, Application destApplication) {
return Mono.zip(
themeService.cloneThemeToApplication(srcApplication.getEditModeThemeId(), destApplication),
themeService.cloneThemeToApplication(srcApplication.getPublishedModeThemeId(), destApplication)
).flatMap(themes -> {
Theme editModeTheme = themes.getT1();
Theme publishedModeTheme = themes.getT2();
return applicationService.setAppTheme(
destApplication.getId(),
editModeTheme.getId(),
publishedModeTheme.getId(),
AclPermission.MANAGE_APPLICATIONS
);
});
}
private Mono<Application> cloneApplicationDocument(Application application) {
if (!StringUtils.hasText(application.getName())) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.NAME));

View File

@ -37,7 +37,6 @@ import com.appsmith.server.repositories.DatasourceRepository;
import com.appsmith.server.repositories.NewActionRepository;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.repositories.ThemeRepository;
import com.appsmith.server.services.ActionCollectionService;
import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.ApplicationService;
@ -47,6 +46,7 @@ import com.appsmith.server.services.NewPageService;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.SequenceService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.ThemeService;
import com.appsmith.server.solutions.ExamplesOrganizationCloner;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -77,6 +77,7 @@ import static com.appsmith.server.acl.AclPermission.EXPORT_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
import static com.appsmith.server.acl.AclPermission.READ_THEMES;
@Slf4j
@RequiredArgsConstructor
@ -97,7 +98,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
private final ExamplesOrganizationCloner examplesOrganizationCloner;
private final ActionCollectionRepository actionCollectionRepository;
private final ActionCollectionService actionCollectionService;
private final ThemeRepository themeRepository;
private final ThemeService themeService;
private static final Set<MediaType> ALLOWED_CONTENT_TYPES = Set.of(MediaType.APPLICATION_JSON);
private static final String INVALID_JSON_FILE = "invalid json file";
@ -153,8 +154,8 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
return plugin;
})
.then(applicationMono)
.flatMap(application -> themeRepository.findById(application.getEditModeThemeId())
.zipWith(themeRepository.findById(application.getPublishedModeThemeId()))
.flatMap(application -> themeService.getThemeById(application.getEditModeThemeId(), READ_THEMES)
.zipWith(themeService.getThemeById(application.getPublishedModeThemeId(), READ_THEMES))
.map(themesTuple -> {
Theme editModeTheme = exportTheme(themesTuple.getT1());
Theme publishedModeTheme = exportTheme(themesTuple.getT2());
@ -581,7 +582,6 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
pluginMap.put(pluginReference, plugin.getId());
return plugin;
})
.then(importThemes(importedApplication, importedDoc))
.then(organizationService.findById(organizationId, AclPermission.ORGANIZATION_MANAGE_APPLICATIONS))
.switchIfEmpty(Mono.error(
new AppsmithException(AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.ORGANIZATION, organizationId))
@ -707,6 +707,7 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
.then(applicationService.save(importedApplication));
})
)
.flatMap(savedAPP -> importThemes(savedAPP, importedDoc))
.flatMap(savedApp -> {
importedApplication.setId(savedApp.getId());
if (savedApp.getGitApplicationMetadata() != null) {
@ -1524,26 +1525,23 @@ public class ImportExportApplicationServiceCEImpl implements ImportExportApplica
}
private Mono<Application> importThemes(Application application, ApplicationJson importedApplicationJson) {
Mono<Theme> importedEditModeTheme = getOrSaveTheme(importedApplicationJson.getEditModeTheme());
Mono<Theme> importedPublishedModeTheme = getOrSaveTheme(importedApplicationJson.getPublishedTheme());
Mono<Theme> importedEditModeTheme = themeService.getOrSaveTheme(importedApplicationJson.getEditModeTheme(), application);
Mono<Theme> importedPublishedModeTheme = themeService.getOrSaveTheme(importedApplicationJson.getPublishedTheme(), application);
return Mono.zip(importedEditModeTheme, importedPublishedModeTheme).map(importedThemesTuple -> {
application.setEditModeThemeId(importedThemesTuple.getT1().getId());
application.setPublishedModeThemeId(importedThemesTuple.getT2().getId());
return application;
return Mono.zip(importedEditModeTheme, importedPublishedModeTheme).flatMap(importedThemesTuple -> {
String editModeThemeId = importedThemesTuple.getT1().getId();
String publishedModeThemeId = importedThemesTuple.getT2().getId();
application.setEditModeThemeId(editModeThemeId);
application.setPublishedModeThemeId(publishedModeThemeId);
// this will update the theme id in DB
// also returning the updated application object so that theme id are available to the next pipeline
return applicationService.setAppTheme(
application.getId(), editModeThemeId, publishedModeThemeId, MANAGE_APPLICATIONS
).thenReturn(application);
});
}
private Mono<Theme> getOrSaveTheme(Theme theme) {
if(theme == null) { // this application was exported without theme, assign the legacy theme to it
return themeRepository.getSystemThemeByName(Theme.LEGACY_THEME_NAME); // return the default theme
} else if (theme.isSystemTheme()) {
return themeRepository.getSystemThemeByName(theme.getName());
} else {
return themeRepository.save(theme);
}
}
private void removeUnwantedFieldsFromApplicationDuringExport(Application application) {
application.setOrganizationId(null);
application.setPages(null);

View File

@ -10,8 +10,7 @@
"appBorderRadius": {
"none": "0px",
"md": "0.375rem",
"lg": "1.5rem",
"full": "9999px"
"lg": "1.5rem"
}
},
"boxShadow": {
@ -41,135 +40,216 @@
"AUDIO_RECORDER_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"BUTTON_WIDGET": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"BUTTON_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CAMERA_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "none"
},
"CHART_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"CHECKBOX_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CHECKBOX_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CONTAINER_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"CIRCULAR_PROGRESS_WIDGET": {
"fillColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"CURRENCY_INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"PHONE_INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"DATE_PICKER_WIDGET2": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"FILE_PICKER_WIDGET_V2": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"FORM_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"FORM_BUTTON_WIDGET": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"ICON_BUTTON_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"IFRAME_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"IMAGE_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"INPUT_WIDGET_V2": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"LIST_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MAP_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MAP_CHART_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MENU_BUTTON_WIDGET": {
"menuColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MODAL_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MULTI_SELECT_TREE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MULTI_SELECT_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"DROP_DOWN_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"PROGRESSBAR_WIDGET": {
"fillColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"RATE_WIDGET": {
"activeColor": "{{appsmith.theme.colors.primaryColor}}"
},
"RADIO_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}"
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"boxShadow": "none"
},
"RICH_TEXT_EDITOR_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"STATBOX_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"SWITCH_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"boxShadow": "none"
},
"SWITCH_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}"
},
"SELECT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"TABLE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"TABS_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"TEXT_WIDGET": {
},
"VIDEO_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"SINGLE_SELECT_TREE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
}
},
"properties": {
@ -199,8 +279,7 @@
"appBorderRadius": {
"none": "0px",
"md": "0.375rem",
"lg": "1.5rem",
"full": "9999px"
"lg": "1.5rem"
}
},
"boxShadow": {
@ -230,135 +309,216 @@
"AUDIO_RECORDER_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"BUTTON_WIDGET": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"BUTTON_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CAMERA_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "none"
},
"CHART_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"CHECKBOX_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CHECKBOX_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CONTAINER_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"CIRCULAR_PROGRESS_WIDGET": {
"fillColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"CURRENCY_INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"PHONE_INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"DATE_PICKER_WIDGET2": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"FILE_PICKER_WIDGET_V2": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"FORM_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"FORM_BUTTON_WIDGET": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"ICON_BUTTON_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"IFRAME_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"IMAGE_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"INPUT_WIDGET_V2": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"LIST_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MAP_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MAP_CHART_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MENU_BUTTON_WIDGET": {
"menuColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MODAL_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MULTI_SELECT_TREE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MULTI_SELECT_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"DROP_DOWN_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"PROGRESSBAR_WIDGET": {
"fillColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"RATE_WIDGET": {
"activeColor": "{{appsmith.theme.colors.primaryColor}}"
},
"RADIO_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}"
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"boxShadow": "none"
},
"RICH_TEXT_EDITOR_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"STATBOX_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"SWITCH_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"boxShadow": "none"
},
"SWITCH_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}"
},
"SELECT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"TABLE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"TABS_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"TEXT_WIDGET": {
},
"VIDEO_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"SINGLE_SELECT_TREE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
}
},
"properties": {
@ -388,8 +548,7 @@
"appBorderRadius": {
"none": "0px",
"md": "0.375rem",
"lg": "1.5rem",
"full": "9999px"
"lg": "1.5rem"
}
},
"boxShadow": {
@ -419,135 +578,216 @@
"AUDIO_RECORDER_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"BUTTON_WIDGET": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"BUTTON_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CAMERA_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "none"
},
"CHART_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"CHECKBOX_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CHECKBOX_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CONTAINER_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"CIRCULAR_PROGRESS_WIDGET": {
"fillColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"CURRENCY_INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"PHONE_INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"DATE_PICKER_WIDGET2": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"FILE_PICKER_WIDGET_V2": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"FORM_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"FORM_BUTTON_WIDGET": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"ICON_BUTTON_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"IFRAME_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"IMAGE_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"INPUT_WIDGET_V2": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"LIST_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MAP_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MAP_CHART_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MENU_BUTTON_WIDGET": {
"menuColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MODAL_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MULTI_SELECT_TREE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MULTI_SELECT_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"DROP_DOWN_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"PROGRESSBAR_WIDGET": {
"fillColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"RATE_WIDGET": {
"activeColor": "{{appsmith.theme.colors.primaryColor}}"
},
"RADIO_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}"
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"boxShadow": "none"
},
"RICH_TEXT_EDITOR_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"STATBOX_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"SWITCH_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"boxShadow": "none"
},
"SWITCH_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}"
},
"SELECT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"TABLE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"TABS_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"TEXT_WIDGET": {
},
"VIDEO_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"SINGLE_SELECT_TREE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
}
},
"properties": {
@ -577,8 +817,7 @@
"appBorderRadius": {
"none": "0px",
"md": "0.375rem",
"lg": "1.5rem",
"full": "9999px"
"lg": "1.5rem"
}
},
"boxShadow": {
@ -608,135 +847,216 @@
"AUDIO_RECORDER_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"BUTTON_WIDGET": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"BUTTON_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CAMERA_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "none"
},
"CHART_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"CHECKBOX_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CHECKBOX_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"CONTAINER_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"CIRCULAR_PROGRESS_WIDGET": {
"fillColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"CURRENCY_INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"PHONE_INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"DATE_PICKER_WIDGET2": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"FILE_PICKER_WIDGET_V2": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"FORM_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"FORM_BUTTON_WIDGET": {
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"ICON_BUTTON_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"buttonColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"IFRAME_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"IMAGE_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"INPUT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"INPUT_WIDGET_V2": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"LIST_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MAP_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MAP_CHART_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"MENU_BUTTON_WIDGET": {
"menuColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MODAL_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MULTI_SELECT_TREE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"MULTI_SELECT_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"DROP_DOWN_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"PROGRESSBAR_WIDGET": {
"fillColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
},
"RATE_WIDGET": {
"activeColor": "{{appsmith.theme.colors.primaryColor}}"
},
"RADIO_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}"
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"boxShadow": "none"
},
"RICH_TEXT_EDITOR_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"STATBOX_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"SWITCH_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}",
"boxShadow": "none"
},
"SWITCH_GROUP_WIDGET": {
"backgroundColor": "{{appsmith.theme.colors.primaryColor}}"
},
"SELECT_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
},
"TABLE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"TABS_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"TEXT_WIDGET": {
},
"VIDEO_WIDGET": {
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "{{appsmith.theme.boxShadow.appBoxShadow}}"
},
"SINGLE_SELECT_TREE_WIDGET": {
"primaryColor": "{{appsmith.theme.colors.primaryColor}}",
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}"
"borderRadius": "{{appsmith.theme.borderRadius.appBorderRadius}}",
"fontFamily": "{{appsmith.theme.fontFamily.appFont}}",
"boxShadow": "none"
}
},
"properties": {
@ -745,7 +1065,7 @@
"backgroundColor": "#F6F6F6"
},
"borderRadius": {
"appBorderRadius": "1rem"
"appBorderRadius": "1.5rem"
},
"boxShadow": {
"appBoxShadow": "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)"
@ -755,4 +1075,4 @@
}
}
}
]
]

View File

@ -46,7 +46,7 @@ public class PolicyUtilsTest {
CommentThread commentThread = new CommentThread();
commentThread.setApplicationId(testApplicationId);
Map<String, Policy> commentThreadPolicies = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD, AclPermission.COMMENT_ON_THREAD), "api_user"
Set.of(AclPermission.MANAGE_THREADS, AclPermission.COMMENT_ON_THREADS), "api_user"
);
commentThread.setPolicies(Set.copyOf(commentThreadPolicies.values()));
Mono<CommentThread> saveThreadMono = commentThreadRepository.save(commentThread);
@ -54,7 +54,7 @@ public class PolicyUtilsTest {
// add a new user and update the policies of the new user
String newUserName = "new_test_user";
Map<String, Policy> commentThreadPoliciesForNewUser = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.COMMENT_ON_THREAD), newUserName
Set.of(AclPermission.COMMENT_ON_THREADS), newUserName
);
Flux<CommentThread> updateCommentThreads = policyUtils.updateCommentThreadPermissions(
testApplicationId, commentThreadPoliciesForNewUser, newUserName, true
@ -64,7 +64,7 @@ public class PolicyUtilsTest {
Mono<List<CommentThread>> applicationCommentList = saveThreadMono
.thenMany(updateCommentThreads)
.collectList()
.thenMany(commentThreadRepository.findByApplicationId(testApplicationId, AclPermission.READ_THREAD))
.thenMany(commentThreadRepository.findByApplicationId(testApplicationId, AclPermission.READ_THREADS))
.collectList();
StepVerifier.create(applicationCommentList)
@ -72,12 +72,12 @@ public class PolicyUtilsTest {
assertThat(commentThreads.size()).isEqualTo(1);
CommentThread commentThread1 = commentThreads.get(0);
Set<Policy> policies = commentThread1.getPolicies();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.MANAGE_THREAD.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.MANAGE_THREAD.getValue(), newUserName)).isFalse();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.READ_THREAD.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.READ_THREAD.getValue(), newUserName)).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.COMMENT_ON_THREAD.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.COMMENT_ON_THREAD.getValue(), newUserName)).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.MANAGE_THREADS.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.MANAGE_THREADS.getValue(), newUserName)).isFalse();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.READ_THREADS.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.READ_THREADS.getValue(), newUserName)).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.COMMENT_ON_THREADS.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.COMMENT_ON_THREADS.getValue(), newUserName)).isTrue();
})
.verifyComplete();
}
@ -100,7 +100,7 @@ public class PolicyUtilsTest {
user2.setEmail(newUserName);
Map<String, Policy> commentThreadPolicies = policyUtils.generatePolicyFromPermissionForMultipleUsers(
Set.of(AclPermission.MANAGE_THREAD, AclPermission.COMMENT_ON_THREAD), List.of(user1, user2)
Set.of(AclPermission.MANAGE_THREADS, AclPermission.COMMENT_ON_THREADS), List.of(user1, user2)
);
commentThread.setPolicies(Set.copyOf(commentThreadPolicies.values()));
@ -108,7 +108,7 @@ public class PolicyUtilsTest {
// remove an user and update the policies of the user
Map<String, Policy> commentThreadPoliciesForNewUser = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD, AclPermission.COMMENT_ON_THREAD), newUserName
Set.of(AclPermission.MANAGE_THREADS, AclPermission.COMMENT_ON_THREADS), newUserName
);
Flux<CommentThread> updateCommentThreads = policyUtils.updateCommentThreadPermissions(
testApplicationId, commentThreadPoliciesForNewUser, newUserName, false
@ -118,7 +118,7 @@ public class PolicyUtilsTest {
Mono<List<CommentThread>> applicationCommentList = saveThreadMono
.thenMany(updateCommentThreads)
.collectList()
.thenMany(commentThreadRepository.findByApplicationId(testApplicationId, AclPermission.READ_THREAD))
.thenMany(commentThreadRepository.findByApplicationId(testApplicationId, AclPermission.READ_THREADS))
.collectList();
StepVerifier.create(applicationCommentList)
@ -126,12 +126,12 @@ public class PolicyUtilsTest {
assertThat(commentThreads.size()).isEqualTo(1);
CommentThread commentThread1 = commentThreads.get(0);
Set<Policy> policies = commentThread1.getPolicies();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.MANAGE_THREAD.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.MANAGE_THREAD.getValue(), newUserName)).isFalse();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.READ_THREAD.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.READ_THREAD.getValue(), newUserName)).isFalse();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.COMMENT_ON_THREAD.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.COMMENT_ON_THREAD.getValue(), newUserName)).isFalse();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.MANAGE_THREADS.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.MANAGE_THREADS.getValue(), newUserName)).isFalse();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.READ_THREADS.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.READ_THREADS.getValue(), newUserName)).isFalse();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.COMMENT_ON_THREADS.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(policies, AclPermission.COMMENT_ON_THREADS.getValue(), newUserName)).isFalse();
})
.verifyComplete();
}

View File

@ -40,7 +40,7 @@ public class CustomCommentThreadRepositoryImplTest {
User user = new User();
user.setEmail(userEmail);
Map<String, Policy> policyMap = policyUtils.generatePolicyFromPermission(Set.of(AclPermission.READ_THREAD), user);
Map<String, Policy> policyMap = policyUtils.generatePolicyFromPermission(Set.of(AclPermission.READ_THREADS), user);
thread.setPolicies(Set.copyOf(policyMap.values()));
return thread;
}
@ -54,7 +54,7 @@ public class CustomCommentThreadRepositoryImplTest {
user.setEmail(userEmail);
Map<String, Policy> policyMap = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.MANAGE_THREAD, AclPermission.COMMENT_ON_THREAD), user);
Set.of(AclPermission.MANAGE_THREADS, AclPermission.COMMENT_ON_THREADS), user);
HashSet<Policy> policySet = new HashSet<>();
// not using Set.of here because the caller function may need to add more policies
@ -167,7 +167,7 @@ public class CustomCommentThreadRepositoryImplTest {
CommentThreadFilterDTO filterDTO = new CommentThreadFilterDTO();
filterDTO.setApplicationId("sample-application-id-1");
filterDTO.setResolved(false);
return commentThreadRepository.find(filterDTO, AclPermission.READ_THREAD).collectList();
return commentThreadRepository.find(filterDTO, AclPermission.READ_THREADS).collectList();
});
StepVerifier.create(listMono).assertNext(
@ -255,7 +255,7 @@ public class CustomCommentThreadRepositoryImplTest {
Mono<Map<String, Collection<CommentThread>>> pageIdThreadMono = commentThreadRepository.saveAll(threads)
.collectList()
.then(commentThreadRepository.archiveByPageId(pageOneId, ApplicationMode.EDIT))
.thenMany(commentThreadRepository.findByApplicationId(applicationId, AclPermission.READ_THREAD))
.thenMany(commentThreadRepository.findByApplicationId(applicationId, AclPermission.READ_THREADS))
.collectMultimap(CommentThread::getPageId);
StepVerifier.create(pageIdThreadMono)
@ -282,7 +282,7 @@ public class CustomCommentThreadRepositoryImplTest {
// add api_user to thread policy with read thread permission
for(Policy policy: thread.getPolicies()) {
if(policy.getPermission().equals(AclPermission.READ_THREAD.getValue())) {
if(policy.getPermission().equals(AclPermission.READ_THREADS.getValue())) {
Set<String> users = new HashSet<>();
users.addAll(policy.getUsers());
users.add("api_user");
@ -292,7 +292,7 @@ public class CustomCommentThreadRepositoryImplTest {
Mono<Map<String, Collection<CommentThread>>> pageIdThreadMono = commentThreadRepository.save(thread)
.then(commentThreadRepository.archiveByPageId(testPageId, ApplicationMode.EDIT)) // this will do nothing
.thenMany(commentThreadRepository.findByApplicationId(applicationId, AclPermission.READ_THREAD))
.thenMany(commentThreadRepository.findByApplicationId(applicationId, AclPermission.READ_THREADS))
.collectMultimap(CommentThread::getPageId);
StepVerifier.create(pageIdThreadMono)

View File

@ -1,6 +1,8 @@
package com.appsmith.server.repositories;
import com.appsmith.external.models.Policy;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.helpers.PolicyUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@ -11,7 +13,10 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.appsmith.server.acl.AclPermission.READ_THEMES;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ -21,10 +26,20 @@ public class CustomThemeRepositoryTest {
@Autowired
ThemeRepository themeRepository;
@Autowired
PolicyUtils policyUtils;
@WithUserDetails("api_user")
@Test
public void getSystemThemes_WhenThemesExists_ReturnsSystemThemes() {
Mono<List<Theme>> systemThemesMono = themeRepository.save(new Theme())
String testAppId = "second-app-id";
Theme firstAppTheme = new Theme();
firstAppTheme.setApplicationId("first-app-id");
Theme secondAppTheme = new Theme();
secondAppTheme.setApplicationId(testAppId);
Mono<List<Theme>> systemThemesMono = themeRepository.saveAll(List.of(firstAppTheme, secondAppTheme))
.then(themeRepository.getSystemThemes().collectList());
StepVerifier.create(systemThemesMono).assertNext(themes -> {
@ -32,6 +47,28 @@ public class CustomThemeRepositoryTest {
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void getApplicationThemes_WhenThemesExists_ReturnsAppThemes() {
Map<String, Policy> themePolicies = policyUtils.generatePolicyFromPermission(Set.of(READ_THEMES), "api_user");
String testAppId = "second-app-id";
Theme firstAppTheme = new Theme();
firstAppTheme.setApplicationId("first-app-id");
firstAppTheme.setPolicies(Set.of(themePolicies.get(READ_THEMES.getValue())));
Theme secondAppTheme = new Theme();
secondAppTheme.setApplicationId(testAppId);
secondAppTheme.setPolicies(Set.of(themePolicies.get(READ_THEMES.getValue())));
Mono<List<Theme>> systemThemesMono = themeRepository.saveAll(List.of(firstAppTheme, secondAppTheme))
.then(themeRepository.getApplicationThemes(testAppId, READ_THEMES).collectList());
StepVerifier.create(systemThemesMono).assertNext(themes -> {
assertThat(themes.size()).isEqualTo(5); // 4 system themes were created from db migration
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void getSystemThemeByName_WhenNameMatches_ReturnsTheme() {

View File

@ -21,6 +21,7 @@ import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.User;
import com.appsmith.server.dtos.ActionCollectionDTO;
import com.appsmith.server.dtos.ActionDTO;
@ -82,6 +83,7 @@ import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES;
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
import static com.appsmith.server.acl.AclPermission.MANAGE_THEMES;
import static com.appsmith.server.acl.AclPermission.READ_ACTIONS;
import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.READ_DATASOURCES;
@ -2252,4 +2254,42 @@ public class ApplicationServiceTest {
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void cloneApplication_WithCustomSavedTheme_ThemesAlsoCopied() {
Application testApplication = new Application();
String appName = "cloneApplication_WithCustomSavedTheme_ThemesAlsoCopied";
testApplication.setName(appName);
Theme theme = new Theme();
theme.setName("Custom theme");
Mono<Theme> createTheme = themeService.create(theme);
Mono<Tuple2<Theme, Tuple2<Application, Application>>> tuple2Application = createTheme
.then(applicationPageService.createApplication(testApplication, orgId))
.flatMap(application ->
themeService.updateTheme(application.getId(), theme).then(
themeService.persistCurrentTheme(application.getId(), new Theme())
.flatMap(theme1 -> Mono.zip(
applicationPageService.cloneApplication(application.getId(), null),
Mono.just(application))
)
)
).flatMap(objects ->
themeService.getThemeById(objects.getT1().getEditModeThemeId(), MANAGE_THEMES)
.zipWith(Mono.just(objects))
);
StepVerifier.create(tuple2Application)
.assertNext(objects -> {
Theme clonedTheme = objects.getT1();
Application clonedApp = objects.getT2().getT1();
Application srcApp = objects.getT2().getT2();
assertThat(clonedApp.getEditModeThemeId()).isNotEqualTo(srcApp.getEditModeThemeId());
assertThat(clonedTheme.getApplicationId()).isNull();
assertThat(clonedTheme.getOrganizationId()).isNull();
})
.verifyComplete();
}
}

View File

@ -141,15 +141,15 @@ public class CommentServiceTest {
assertThat(thread.getId()).isNotEmpty();
//assertThat(thread.getResolved()).isNull();
assertThat(thread.getPolicies()).containsExactlyInAnyOrder(
Policy.builder().permission(AclPermission.READ_THREAD.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build(),
Policy.builder().permission(AclPermission.MANAGE_THREAD.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build(),
Policy.builder().permission(AclPermission.COMMENT_ON_THREAD.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build()
Policy.builder().permission(AclPermission.READ_THREADS.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build(),
Policy.builder().permission(AclPermission.MANAGE_THREADS.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build(),
Policy.builder().permission(AclPermission.COMMENT_ON_THREADS.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build()
);
assertThat(thread.getComments()).hasSize(2); // one comment is from bot
assertThat(thread.getComments().get(0).getBody()).isEqualTo(makePlainTextComment("comment one").getBody());
assertThat(thread.getComments().get(0).getPolicies()).containsExactlyInAnyOrder(
Policy.builder().permission(AclPermission.MANAGE_COMMENT.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build(),
Policy.builder().permission(AclPermission.READ_COMMENT.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build()
Policy.builder().permission(AclPermission.MANAGE_COMMENTS.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build(),
Policy.builder().permission(AclPermission.READ_COMMENTS.getValue()).users(Set.of("api_user")).groups(Collections.emptySet()).build()
);
assertThat(threadsInApp).hasSize(1);
@ -325,7 +325,7 @@ public class CommentServiceTest {
User user = new User();
user.setEmail("api_user");
Map<String, Policy> stringPolicyMap = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.READ_THREAD),
Set.of(AclPermission.READ_THREADS),
user
);
Set<Policy> policies = Set.copyOf(stringPolicyMap.values());
@ -369,7 +369,7 @@ public class CommentServiceTest {
public void create_WhenThreadIsResolvedAndAlreadyViewed_ThreadIsUnresolvedAndUnread() {
// create a thread first with resolved=true
Collection<Policy> threadPolicies = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.COMMENT_ON_THREAD),
Set.of(AclPermission.COMMENT_ON_THREADS),
"api_user"
).values();
@ -587,7 +587,7 @@ public class CommentServiceTest {
// create a thread first with resolved=true
Collection<Policy> threadPolicies = policyUtils.generatePolicyFromPermission(
Set.of(AclPermission.COMMENT_ON_THREAD),
Set.of(AclPermission.COMMENT_ON_THREADS),
"api_user"
).values();
CommentThread commentThread = new CommentThread();

View File

@ -2,13 +2,15 @@ package com.appsmith.server.services;
import com.appsmith.external.models.Policy;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.dtos.ApplicationAccessDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.PolicyUtils;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.ThemeRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
@ -19,12 +21,17 @@ import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.function.Tuple2;
import reactor.util.function.Tuple3;
import reactor.util.function.Tuples;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import static com.appsmith.server.acl.AclPermission.MAKE_PUBLIC_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_THEMES;
import static com.appsmith.server.acl.AclPermission.READ_THEMES;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest
@ -38,7 +45,7 @@ public class ThemeServiceTest {
ApplicationRepository applicationRepository;
@Autowired
ThemeRepository themeRepository;
ApplicationService applicationService;
@Autowired
private ThemeService themeService;
@ -55,20 +62,20 @@ public class ThemeServiceTest {
@WithUserDetails("api_user")
@Test
public void getApplicationTheme_WhenThemeIsSet_ThemesReturned() {
Mono<Tuple2<Theme, Theme>> applicationThemesMono = themeRepository.getSystemThemeByName("Classic")
.zipWith(themeRepository.getSystemThemeByName("Sharp"))
Mono<Tuple2<Theme, Theme>> applicationThemesMono = themeService.getSystemTheme("Classic")
.zipWith(themeService.getSystemTheme("Sharp"))
.flatMap(themesTuple -> {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(themesTuple.getT1().getId());
application.setPublishedModeThemeId(themesTuple.getT2().getId());
return applicationRepository.save(application);
})
.flatMap(application -> {
return Mono.zip(
themeService.getApplicationTheme(application.getId(), ApplicationMode.EDIT),
themeService.getApplicationTheme(application.getId(), ApplicationMode.PUBLISHED)
);
});
.flatMap(application ->
Mono.zip(
themeService.getApplicationTheme(application.getId(), ApplicationMode.EDIT),
themeService.getApplicationTheme(application.getId(), ApplicationMode.PUBLISHED)
)
);
StepVerifier.create(applicationThemesMono)
.assertNext(themesTuple -> {
@ -82,20 +89,18 @@ public class ThemeServiceTest {
@WithUserDetails("api_user")
@Test
public void getApplicationTheme_WhenUserHasNoPermission_ExceptionThrows() {
Mono<Tuple2<Theme, Theme>> applicationThemesMono = themeRepository.getSystemThemeByName("Classic")
.zipWith(themeRepository.getSystemThemeByName("Sharp"))
Mono<Tuple2<Theme, Theme>> applicationThemesMono = themeService.getSystemTheme("Classic")
.zipWith(themeService.getSystemTheme("Sharp"))
.flatMap(themesTuple -> {
Application application = createApplication("random_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(themesTuple.getT1().getId());
application.setPublishedModeThemeId(themesTuple.getT2().getId());
return applicationRepository.save(application);
})
.flatMap(application -> {
return Mono.zip(
themeService.getApplicationTheme(application.getId(), ApplicationMode.EDIT),
themeService.getApplicationTheme(application.getId(), ApplicationMode.PUBLISHED)
);
});
.flatMap(application -> Mono.zip(
themeService.getApplicationTheme(application.getId(), ApplicationMode.EDIT),
themeService.getApplicationTheme(application.getId(), ApplicationMode.PUBLISHED)
));
StepVerifier.create(applicationThemesMono)
.expectError(AppsmithException.class)
@ -105,7 +110,7 @@ public class ThemeServiceTest {
@WithUserDetails("api_user")
@Test
public void changeCurrentTheme_WhenUserHasPermission_ThemesSetInEditMode() {
Mono<Tuple2<Application, Application>> tuple2Mono = themeRepository.getSystemThemeByName("Classic")
Mono<Tuple2<Application, Application>> tuple2Mono = themeService.getSystemTheme("Classic")
.flatMap(theme -> {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(theme.getId());
@ -113,7 +118,7 @@ public class ThemeServiceTest {
// setting classic theme to edit mode and published mode
return applicationRepository.save(application);
})
.zipWith(themeRepository.getSystemThemeByName("Rounded"))
.zipWith(themeService.getSystemTheme("Rounded"))
.flatMap(tuple -> {
Application application = tuple.getT1();
Theme theme = tuple.getT2();
@ -141,7 +146,7 @@ public class ThemeServiceTest {
@WithUserDetails("api_user")
@Test
public void changeCurrentTheme_WhenUserHasNoPermission_ThrowsException() {
Mono<Theme> themeMono = themeRepository.getSystemThemeByName("Classic")
Mono<Theme> themeMono = themeService.getSystemTheme("Classic")
.flatMap(theme -> {
Application application = createApplication("some_other_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(theme.getId());
@ -157,16 +162,84 @@ public class ThemeServiceTest {
StepVerifier.create(themeMono).expectError(AppsmithException.class).verify();
}
@WithUserDetails("api_user")
@Test
public void changeCurrentTheme_WhenSystemThemeSet_NoNewThemeCreated() {
Mono<String> defaultThemeIdMono = themeService.getDefaultThemeId().cache();
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setOrganizationId("theme-test-org-id");
Mono<Theme> applicationThemeMono = defaultThemeIdMono
.flatMap(defaultThemeId -> {
application.setEditModeThemeId(defaultThemeId);
return applicationRepository.save(application);
})
.flatMap(savedApplication ->
defaultThemeIdMono.flatMap(themeId ->
themeService.changeCurrentTheme(themeId, savedApplication.getId())
.then(themeService.getApplicationTheme(savedApplication.getId(), ApplicationMode.EDIT))
)
);
StepVerifier.create(applicationThemeMono).assertNext(theme -> {
assertThat(theme.isSystemTheme()).isTrue();
assertThat(theme.getApplicationId()).isNull();
assertThat(theme.getOrganizationId()).isNull();
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void changeCurrentTheme_WhenSystemThemeSetOverCustomTheme_NewThemeNotCreatedAndOldOneDeleted() {
Collection<Policy> themePolicies = policyUtils.generatePolicyFromPermission(
Set.of(MANAGE_THEMES), "api_user"
).values();
Theme customTheme = new Theme();
customTheme.setName("my-custom-theme");
customTheme.setPolicies(Set.copyOf(themePolicies));
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setOrganizationId("theme-test-org-id");
Mono<Tuple2<Theme, Theme>> tuple2Mono = themeService.save(customTheme)
.flatMap(savedTheme -> {
application.setEditModeThemeId(savedTheme.getId());
return applicationRepository.save(application);
})
.flatMap(savedApplication ->
themeService.getDefaultThemeId()
.flatMap(themeId -> themeService.changeCurrentTheme(themeId, savedApplication.getId()))
.thenReturn(savedApplication)
).flatMap(application1 ->
// get old theme and new
Mono.zip(
themeService.getApplicationTheme(application1.getId(), ApplicationMode.EDIT),
themeService.getThemeById(application1.getEditModeThemeId(), READ_THEMES)
.defaultIfEmpty(new Theme()) // this should be deleted, return empty theme
)
);
StepVerifier.create(tuple2Mono).assertNext(themeTuple2 -> {
Theme currentTheme = themeTuple2.getT1();
Theme oldTheme = themeTuple2.getT2();
assertThat(currentTheme.isSystemTheme()).isTrue();
assertThat(currentTheme.getApplicationId()).isNull();
assertThat(currentTheme.getOrganizationId()).isNull();
assertThat(oldTheme.getId()).isNull();
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void cloneThemeToApplication_WhenSrcThemeIsSystemTheme_NoNewThemeCreated() {
Application newApplication = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
Mono<Tuple2<Theme, Theme>> newAndOldThemeMono = applicationRepository.save(newApplication)
.zipWith(themeRepository.getSystemThemeByName("Classic"))
.zipWith(themeService.getSystemTheme("Classic"))
.flatMap(applicationAndTheme -> {
Theme theme = applicationAndTheme.getT2();
Application application = applicationAndTheme.getT1();
return themeService.cloneThemeToApplication(theme.getId(), application.getId()).zipWith(Mono.just(theme));
return themeService.cloneThemeToApplication(theme.getId(), application).zipWith(Mono.just(theme));
});
StepVerifier.create(newAndOldThemeMono)
@ -182,13 +255,16 @@ public class ThemeServiceTest {
Application newApplication = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
Theme customTheme = new Theme();
customTheme.setName("custom theme");
customTheme.setPolicies(Set.copyOf(
policyUtils.generatePolicyFromPermission(Set.of(MANAGE_THEMES), "api_user").values()
));
Mono<Tuple2<Theme, Theme>> newAndOldThemeMono = applicationRepository.save(newApplication)
.zipWith(themeRepository.save(customTheme))
.zipWith(themeService.save(customTheme))
.flatMap(applicationAndTheme -> {
Theme theme = applicationAndTheme.getT2();
Application application = applicationAndTheme.getT1();
return themeService.cloneThemeToApplication(theme.getId(), application.getId()).zipWith(Mono.just(theme));
return themeService.cloneThemeToApplication(theme.getId(), application).zipWith(Mono.just(theme));
});
StepVerifier.create(newAndOldThemeMono)
@ -199,14 +275,57 @@ public class ThemeServiceTest {
.verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void cloneThemeToApplication_WhenSrcThemeIsCustomSavedTheme_NewCustomThemeCreated() {
Application srcApplication = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
Mono<Tuple2<Theme, Theme>> newAndOldThemeMono = applicationRepository.save(srcApplication)
.flatMap(application -> {
Theme srcCustomTheme = new Theme();
srcCustomTheme.setName("custom theme");
srcCustomTheme.setApplicationId(application.getId());
srcCustomTheme.setPolicies(Set.copyOf(
policyUtils.generatePolicyFromPermission(Set.of(MANAGE_THEMES), "api_user").values()
));
return themeService.save(srcCustomTheme);
})
.zipWith(applicationRepository.save(createApplication("api_user", Set.of(MANAGE_APPLICATIONS))))
.flatMap(objects -> {
Theme srcTheme = objects.getT1();
Application destApp = objects.getT2();
return Mono.zip(
themeService.cloneThemeToApplication(srcTheme.getId(), destApp),
Mono.just(srcTheme)
);
});
StepVerifier.create(newAndOldThemeMono)
.assertNext(objects -> {
Theme clonnedTheme = objects.getT1();
Theme srcTheme = objects.getT2();
assertThat(clonnedTheme.getId()).isNotEqualTo(srcTheme.getId());
assertThat(clonnedTheme.getName()).isEqualTo(srcTheme.getName());
assertThat(clonnedTheme.getApplicationId()).isNull();
assertThat(clonnedTheme.getOrganizationId()).isNull();
})
.verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void getApplicationTheme_WhenUserHasPermission_ThemeReturned() {
Collection<Policy> themePolicies = policyUtils.generatePolicyFromPermission(
Set.of(MANAGE_THEMES), "api_user"
).values();
Theme customTheme = new Theme();
customTheme.setName("custom theme for edit mode");
customTheme.setPolicies(Set.copyOf(themePolicies));
Mono<Tuple2<Theme, Theme>> applicationThemesMono = themeRepository.save(customTheme)
.zipWith(themeRepository.getSystemThemeByName("classic"))
Mono<Tuple2<Theme, Theme>> applicationThemesMono = themeService.save(customTheme)
.zipWith(themeService.getSystemTheme("classic"))
.flatMap(themes -> {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(themes.getT1().getId());
@ -230,7 +349,7 @@ public class ThemeServiceTest {
@WithUserDetails("api_user")
@Test
public void publishTheme_WhenSystemThemeIsSet_NoNewThemeCreated() {
Mono<Theme> classicThemeMono = themeRepository.getSystemThemeByName("classic").cache();
Mono<Theme> classicThemeMono = themeService.getSystemTheme("classic").cache();
Mono<Tuple2<Application, Theme>> appAndThemeTuple = classicThemeMono
.flatMap(theme -> {
@ -239,9 +358,8 @@ public class ThemeServiceTest {
application.setPublishedModeThemeId("this-id-should-be-overridden");
return applicationRepository.save(application);
}).flatMap(savedApplication ->
themeService.publishTheme(savedApplication.getEditModeThemeId(),
savedApplication.getPublishedModeThemeId(), savedApplication.getId()
).then(applicationRepository.findById(savedApplication.getId()))
themeService.publishTheme(savedApplication.getId())
.then(applicationRepository.findById(savedApplication.getId()))
)
.zipWith(classicThemeMono);
@ -254,23 +372,53 @@ public class ThemeServiceTest {
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void publishTheme_WhenSystemThemeInEditModeAndCustomThemeInPublishedMode_PublisedCopyDeleted() {
Mono<Theme> classicThemeMono = themeService.getSystemTheme("classic").cache();
Theme customTheme = new Theme();
customTheme.setName("published-theme-copy");
Mono<Theme> publishedCustomThemeMono = themeService.save(customTheme);
Mono<Theme> deletedThemeMono = classicThemeMono
.zipWith(publishedCustomThemeMono)
.flatMap(themesTuple -> {
Theme systemTheme = themesTuple.getT1();
Theme savedCustomTheme = themesTuple.getT2();
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(systemTheme.getId());
application.setPublishedModeThemeId(savedCustomTheme.getId());
return applicationRepository.save(application);
}).flatMap(savedApplication ->
themeService.publishTheme(savedApplication.getId())
.then(themeService.getThemeById(savedApplication.getPublishedModeThemeId(), READ_THEMES))
);
StepVerifier.create(deletedThemeMono)
.verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void publishTheme_WhenCustomThemeIsSet_ThemeCopiedForPublishedMode() {
Collection<Policy> themePolicies = policyUtils.generatePolicyFromPermission(
Set.of(MANAGE_THEMES), "api_user"
).values();
Theme customTheme = new Theme();
customTheme.setName("my-custom-theme");
customTheme.setPolicies(Set.copyOf(themePolicies));
Mono<Tuple2<Theme, Theme>> appThemesMono = themeRepository.save(customTheme)
.zipWith(themeRepository.getSystemThemeByName("classic"))
Mono<Tuple2<Theme, Theme>> appThemesMono = themeService.save(customTheme)
.zipWith(themeService.getSystemTheme("classic"))
.flatMap(themes -> {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(themes.getT1().getId()); // custom theme
application.setPublishedModeThemeId(themes.getT2().getId()); // system theme
return applicationRepository.save(application);
}).flatMap(application ->
themeService.publishTheme(application.getEditModeThemeId(),
application.getPublishedModeThemeId(), application.getId()
).then(Mono.zip(
themeService.publishTheme(application.getId()).then(Mono.zip(
themeService.getApplicationTheme(application.getId(), ApplicationMode.EDIT),
themeService.getApplicationTheme(application.getId(), ApplicationMode.PUBLISHED)
))
@ -291,7 +439,7 @@ public class ThemeServiceTest {
Theme customTheme = new Theme();
customTheme.setName("My custom theme");
Mono<Tuple2<Theme, Theme>> appThemesMono = themeRepository.getSystemThemeByName("classic")
Mono<Tuple2<Theme, Theme>> appThemesMono = themeService.getSystemTheme("classic")
.flatMap(theme -> {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(theme.getId()); // system theme
@ -319,12 +467,17 @@ public class ThemeServiceTest {
@WithUserDetails("api_user")
@Test
public void updateTheme_WhenCustomThemeIsSet_ThemeIsOverridden() {
Collection<Policy> themePolicies = policyUtils.generatePolicyFromPermission(
Set.of(MANAGE_THEMES), "api_user"
).values();
Theme customTheme = new Theme();
customTheme.setName("My custom theme");
Mono<Theme> saveCustomThemeMono = themeRepository.save(customTheme);
customTheme.setPolicies(Set.copyOf(themePolicies));
Mono<Theme> saveCustomThemeMono = themeService.save(customTheme);
Mono<Tuple3<Theme, Theme, Application>> appThemesMono = saveCustomThemeMono
.zipWith(themeRepository.getSystemThemeByName("classic"))
.zipWith(themeService.getSystemTheme("classic"))
.flatMap(themes -> {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(themes.getT1().getId()); // custom theme
@ -361,15 +514,14 @@ public class ThemeServiceTest {
@WithUserDetails("api_user")
@Test
public void publishTheme_WhenNoThemeIsSet_SystemDefaultThemeIsSetToPublishedMode() {
Mono<Theme> classicThemeMono = themeRepository.getSystemThemeByName(Theme.LEGACY_THEME_NAME);
Mono<Theme> classicThemeMono = themeService.getSystemTheme(Theme.LEGACY_THEME_NAME);
Mono<Tuple2<Application, Theme>> appAndThemeTuple = applicationRepository.save(
createApplication("api_user", Set.of(MANAGE_APPLICATIONS))
)
.flatMap(savedApplication ->
themeService.publishTheme(savedApplication.getEditModeThemeId(),
savedApplication.getPublishedModeThemeId(), savedApplication.getId()
).then(applicationRepository.findById(savedApplication.getId()))
themeService.publishTheme(savedApplication.getId())
.then(applicationRepository.findById(savedApplication.getId()))
)
.zipWith(classicThemeMono);
@ -380,4 +532,214 @@ public class ThemeServiceTest {
assertThat(application.getPublishedModeThemeId()).isEqualTo(classicSystemTheme.getId());
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void publishTheme_WhenApplicationIsPublic_PublishedThemeIsPublic() {
Collection<Policy> themePolicies = policyUtils.generatePolicyFromPermission(
Set.of(MANAGE_THEMES), "api_user"
).values();
Theme customTheme = new Theme();
customTheme.setName("my-custom-theme");
customTheme.setPolicies(Set.copyOf(themePolicies));
Mono<Theme> appThemesMono = themeService.save(customTheme)
.zipWith(themeService.getSystemTheme("classic"))
.flatMap(themes -> {
Application application = createApplication("api_user",
Set.of(MAKE_PUBLIC_APPLICATIONS, MANAGE_APPLICATIONS));
application.setEditModeThemeId(themes.getT1().getId()); // custom theme
application.setPublishedModeThemeId(themes.getT2().getId()); // system theme
return applicationRepository.save(application);
})
.flatMap(application -> {
// make the application public
ApplicationAccessDTO accessDTO = new ApplicationAccessDTO();
accessDTO.setPublicAccess(true);
return applicationService.changeViewAccess(application.getId(), accessDTO);
})
.flatMap(application ->
themeService.publishTheme(application.getId()).then(
themeService.getApplicationTheme(application.getId(), ApplicationMode.PUBLISHED)
)
);
StepVerifier.create(appThemesMono)
.assertNext(publishedModeTheme -> {
Boolean permissionPresentForAnonymousUser = policyUtils.isPermissionPresentForUser(
publishedModeTheme.getPolicies(), READ_THEMES.getValue(), FieldName.ANONYMOUS_USER
);
assertThat(permissionPresentForAnonymousUser).isTrue();
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void persistCurrentTheme_WhenCustomThemeIsSet_NewApplicationThemeCreated() {
Collection<Policy> themePolicies = policyUtils.generatePolicyFromPermission(
Set.of(MANAGE_THEMES), "api_user"
).values();
Theme customTheme = new Theme();
customTheme.setName("Classic");
customTheme.setPolicies(Set.copyOf(themePolicies));
Mono<Tuple3<List<Theme>, Theme, Application>> tuple3Mono = themeService.save(customTheme).flatMap(theme -> {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setEditModeThemeId(theme.getId());
application.setOrganizationId("theme-test-org-id");
return applicationRepository.save(application);
}).flatMap(application -> {
Theme theme = new Theme();
theme.setName("My custom theme");
return themeService.persistCurrentTheme(application.getId(), theme)
.map(theme1 -> Tuples.of(theme1, application));
}).flatMap(persistedThemeAndApp ->
themeService.getApplicationThemes(persistedThemeAndApp.getT2().getId()).collectList()
.map(themes -> Tuples.of(themes, persistedThemeAndApp.getT1(), persistedThemeAndApp.getT2()))
);
StepVerifier.create(tuple3Mono).assertNext(tuple3 -> {
List<Theme> availableThemes = tuple3.getT1();
Theme persistedTheme = tuple3.getT2();
Application application = tuple3.getT3();
assertThat(availableThemes.size()).isEqualTo(5); // one custom theme + 4 system themes
assertThat(persistedTheme.getApplicationId()).isNotEmpty(); // theme should have application id set
assertThat(persistedTheme.getOrganizationId()).isEqualTo("theme-test-org-id"); // theme should have org id set
assertThat(policyUtils.isPermissionPresentForUser(
persistedTheme.getPolicies(), READ_THEMES.getValue(), "api_user")
).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(
persistedTheme.getPolicies(), MANAGE_THEMES.getValue(), "api_user")
).isTrue();
assertThat(application.getEditModeThemeId()).isNotEqualTo(persistedTheme.getId()); // a new copy should be created
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void persistCurrentTheme_WhenSystemThemeIsSet_NewApplicationThemeCreated() {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setOrganizationId("theme-test-org-id");
Mono<Tuple2<List<Theme>, Theme>> tuple2Mono = themeService.getDefaultThemeId()
.flatMap(defaultThemeId -> {
application.setEditModeThemeId(defaultThemeId);
return applicationRepository.save(application);
})
.flatMap(savedApplication -> {
Theme theme = new Theme();
theme.setName("My custom theme");
return themeService.persistCurrentTheme(savedApplication.getId(), theme)
.map(theme1 -> Tuples.of(theme1, savedApplication.getId()));
}).flatMap(persistedThemeAndAppId ->
themeService.getApplicationThemes(persistedThemeAndAppId.getT2()).collectList()
.map(themes -> Tuples.of(themes, persistedThemeAndAppId.getT1()))
);
StepVerifier.create(tuple2Mono).assertNext(tuple2 -> {
List<Theme> availableThemes = tuple2.getT1();
Theme currentTheme = tuple2.getT2();
assertThat(availableThemes.size()).isEqualTo(5); // one custom theme + 4 system themes
assertThat(currentTheme.isSystemTheme()).isFalse();
assertThat(currentTheme.getApplicationId()).isNotEmpty(); // theme should have application id set
assertThat(currentTheme.getOrganizationId()).isEqualTo("theme-test-org-id"); // theme should have org id set
assertThat(policyUtils.isPermissionPresentForUser(currentTheme.getPolicies(), READ_THEMES.getValue(), "api_user")).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(currentTheme.getPolicies(), MANAGE_THEMES.getValue(), "api_user")).isTrue();
}).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void delete_WhenSystemTheme_NotAllowed() {
StepVerifier.create(themeService.getDefaultThemeId().flatMap(themeService::delete))
.expectError(AppsmithException.class)
.verify();
}
@WithUserDetails("api_user")
@Test
public void delete_WhenUnsavedCustomizedTheme_NotAllowed() {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
Mono<Theme> deleteThemeMono = themeService.getDefaultThemeId()
.flatMap(s -> {
application.setEditModeThemeId(s);
return applicationRepository.save(application);
})
.flatMap(savedApplication -> {
Theme themeCustomization = new Theme();
themeCustomization.setName("Updated name");
return themeService.updateTheme(savedApplication.getId(), themeCustomization);
}).flatMap(customizedTheme -> themeService.delete(customizedTheme.getId()));
StepVerifier.create(deleteThemeMono)
.expectErrorMessage(AppsmithError.UNSUPPORTED_OPERATION.getMessage())
.verify();
}
@WithUserDetails("api_user")
@Test
public void delete_WhenSavedCustomizedTheme_ThemeIsDeleted() {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
Mono<Theme> deleteThemeMono = themeService.getDefaultThemeId()
.flatMap(s -> {
application.setEditModeThemeId(s);
return applicationRepository.save(application);
})
.flatMap(savedApplication -> {
Theme themeCustomization = new Theme();
themeCustomization.setName("Updated name");
return themeService.persistCurrentTheme(savedApplication.getId(), themeCustomization);
})
.flatMap(customizedTheme -> themeService.delete(customizedTheme.getId())
.then(themeService.getThemeById(customizedTheme.getId(), READ_THEMES)));
StepVerifier.create(deleteThemeMono).verifyComplete();
}
@WithUserDetails("api_user")
@Test
public void updateName_WhenSystemTheme_NotAllowed() {
Mono<Theme> updateThemeNameMono = themeService.getDefaultThemeId().flatMap(themeId -> {
Theme theme = new Theme();
theme.setName("My theme");
return themeService.updateName(themeId, theme);
});
StepVerifier.create(updateThemeNameMono).expectError(AppsmithException.class).verify();
}
@WithUserDetails("api_user")
@Test
public void updateName_WhenCustomTheme_NameUpdated() {
Application application = createApplication("api_user", Set.of(MANAGE_APPLICATIONS));
application.setOrganizationId("test-org");
Mono<Theme> updateThemeNameMono = themeService.getDefaultThemeId()
.flatMap(s -> {
application.setEditModeThemeId(s);
return applicationRepository.save(application);
})
.flatMap(savedApplication -> {
Theme themeCustomization = new Theme();
themeCustomization.setName("old name");
return themeService.persistCurrentTheme(savedApplication.getId(), themeCustomization);
})
.flatMap(customizedTheme -> {
Theme theme = new Theme();
theme.setName("new name");
return themeService.updateName(customizedTheme.getId(), theme)
.then(themeService.getThemeById(customizedTheme.getId(), READ_THEMES));
});
StepVerifier.create(updateThemeNameMono).assertNext(theme -> {
assertThat(theme.getName()).isEqualTo("new name");
assertThat(theme.isSystemTheme()).isFalse();
assertThat(theme.getApplicationId()).isNotNull();
assertThat(theme.getOrganizationId()).isEqualTo("test-org");
assertThat(theme.getConfig()).isNotNull();
}).verifyComplete();
}
}

View File

@ -277,10 +277,10 @@ class UserOrganizationServiceTest {
StepVerifier.create(commentThreadMono).assertNext(commentThread -> {
Set<Policy> policies = commentThread.getPolicies();
assertThat(policyUtils.isPermissionPresentForUser(
policies, AclPermission.READ_THREAD.getValue(), "test_developer"
policies, AclPermission.READ_THREADS.getValue(), "test_developer"
)).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(
policies, AclPermission.READ_THREAD.getValue(), "api_user"
policies, AclPermission.READ_THREADS.getValue(), "api_user"
)).isTrue();
}).verifyComplete();
}
@ -308,10 +308,10 @@ class UserOrganizationServiceTest {
StepVerifier.create(commentThreadMono).assertNext(commentThread -> {
Set<Policy> policies = commentThread.getPolicies();
assertThat(policyUtils.isPermissionPresentForUser(
policies, AclPermission.READ_THREAD.getValue(), "test_developer"
policies, AclPermission.READ_THREADS.getValue(), "test_developer"
)).isFalse();
assertThat(policyUtils.isPermissionPresentForUser(
policies, AclPermission.READ_THREAD.getValue(), "api_user"
policies, AclPermission.READ_THREADS.getValue(), "api_user"
)).isTrue();
}).verifyComplete();
}
@ -346,13 +346,13 @@ class UserOrganizationServiceTest {
StepVerifier.create(saveUserMono.then(commentThreadMono)).assertNext(commentThread -> {
Set<Policy> policies = commentThread.getPolicies();
assertThat(policyUtils.isPermissionPresentForUser(
policies, AclPermission.READ_THREAD.getValue(), "test_developer"
policies, AclPermission.READ_THREADS.getValue(), "test_developer"
)).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(
policies, AclPermission.READ_THREAD.getValue(), "new_test_user"
policies, AclPermission.READ_THREADS.getValue(), "new_test_user"
)).isTrue();
assertThat(policyUtils.isPermissionPresentForUser(
policies, AclPermission.READ_THREAD.getValue(), "api_user"
policies, AclPermission.READ_THREADS.getValue(), "api_user"
)).isTrue();
}).verifyComplete();
}

View File

@ -9,12 +9,14 @@ import com.appsmith.server.acl.AppsmithRole;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationMode;
import com.appsmith.server.domains.Layout;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Organization;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.PluginType;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.dtos.ActionCollectionDTO;
import com.appsmith.server.dtos.ActionDTO;
import com.appsmith.server.dtos.InviteUsersDTO;
@ -34,6 +36,7 @@ import com.appsmith.server.services.NewActionService;
import com.appsmith.server.services.NewPageService;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.ThemeService;
import com.appsmith.server.services.UserService;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -60,6 +63,8 @@ import org.springframework.util.LinkedMultiValueMap;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.function.Tuple3;
import reactor.util.function.Tuple4;
import java.time.Duration;
import java.util.ArrayList;
@ -67,6 +72,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import static com.appsmith.server.acl.AclPermission.READ_ACTIONS;
import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS;
@ -137,6 +143,9 @@ public class ApplicationForkingServiceTests {
@Autowired
private LayoutCollectionService layoutCollectionService;
@Autowired
private ThemeService themeService;
private static String sourceAppId;
private static String testUserOrgId;
@ -484,6 +493,192 @@ public class ApplicationForkingServiceTests {
}
@Test
@WithUserDetails("api_user")
public void forkApplicationToOrganization_WhenAppHasUnsavedThemeCustomization_ForkedWithCustomizations() {
String uniqueString = UUID.randomUUID().toString();
Organization organization = new Organization();
organization.setName("org_" + uniqueString);
Mono<Tuple4<Theme, Theme, Application, Application>> tuple4Mono = organizationService.create(organization)
.flatMap(createdOrg -> {
Application application = new Application();
application.setName("app_" + uniqueString);
return applicationPageService.createApplication(application, createdOrg.getId());
}).flatMap(srcApplication -> {
Theme theme = new Theme();
theme.setName("theme_" + uniqueString);
return themeService.updateTheme(srcApplication.getId(), theme)
.then(applicationService.findById(srcApplication.getId()));
}).flatMap(srcApplication -> {
Organization desOrg = new Organization();
desOrg.setName("org_dest_" + uniqueString);
return organizationService.create(desOrg).flatMap(createdOrg ->
applicationForkingService.forkApplicationToOrganization(srcApplication.getId(), createdOrg.getId())
).zipWith(Mono.just(srcApplication));
}).flatMap(applicationTuple2 -> {
Application forkedApp = applicationTuple2.getT1();
Application srcApp = applicationTuple2.getT2();
return Mono.zip(
themeService.getApplicationTheme(forkedApp.getId(), ApplicationMode.EDIT),
themeService.getApplicationTheme(forkedApp.getId(), ApplicationMode.PUBLISHED),
Mono.just(forkedApp),
Mono.just(srcApp)
);
});
StepVerifier.create(tuple4Mono).assertNext(objects -> {
Theme editModeTheme = objects.getT1();
Theme publishedModeTheme = objects.getT2();
Application forkedApp = objects.getT3();
Application srcApp = objects.getT4();
assertThat(forkedApp.getEditModeThemeId()).isEqualTo(editModeTheme.getId());
assertThat(forkedApp.getPublishedModeThemeId()).isEqualTo(publishedModeTheme.getId());
assertThat(forkedApp.getEditModeThemeId()).isNotEqualTo(forkedApp.getPublishedModeThemeId());
// published mode should have the custom theme as we publish after forking the app
assertThat(publishedModeTheme.isSystemTheme()).isFalse();
// published mode theme will have no application id and org id set as the customizations were not saved
assertThat(publishedModeTheme.getOrganizationId()).isNullOrEmpty();
assertThat(publishedModeTheme.getApplicationId()).isNullOrEmpty();
// edit mode theme should be a custom one
assertThat(editModeTheme.isSystemTheme()).isFalse();
// edit mode theme will have no application id and org id set as the customizations were not saved
assertThat(editModeTheme.getOrganizationId()).isNullOrEmpty();
assertThat(editModeTheme.getApplicationId()).isNullOrEmpty();
// forked theme should have the same name as src theme
assertThat(editModeTheme.getName()).isEqualTo("theme_" + uniqueString);
assertThat(publishedModeTheme.getName()).isEqualTo("theme_" + uniqueString);
// forked application should have a new edit mode theme created, should not be same as src app theme
assertThat(srcApp.getEditModeThemeId()).isNotEqualTo(forkedApp.getEditModeThemeId());
assertThat(srcApp.getPublishedModeThemeId()).isNotEqualTo(forkedApp.getPublishedModeThemeId());
}).verifyComplete();
}
@Test
@WithUserDetails("api_user")
public void forkApplicationToOrganization_WhenAppHasSystemTheme_SystemThemeSet() {
String uniqueString = UUID.randomUUID().toString();
Organization organization = new Organization();
organization.setName("org_" + uniqueString);
Mono<Tuple3<Theme, Application, Application>> tuple3Mono = organizationService.create(organization)
.flatMap(createdOrg -> {
Application application = new Application();
application.setName("app_" + uniqueString);
return applicationPageService.createApplication(application, createdOrg.getId());
}).flatMap(srcApplication -> {
Organization desOrg = new Organization();
desOrg.setName("org_dest_" + uniqueString);
return organizationService.create(desOrg).flatMap(createdOrg ->
applicationForkingService.forkApplicationToOrganization(srcApplication.getId(), createdOrg.getId())
).zipWith(Mono.just(srcApplication));
}).flatMap(applicationTuple2 -> {
Application forkedApp = applicationTuple2.getT1();
Application srcApp = applicationTuple2.getT2();
return Mono.zip(
themeService.getApplicationTheme(forkedApp.getId(), ApplicationMode.EDIT),
Mono.just(forkedApp),
Mono.just(srcApp)
);
});
StepVerifier.create(tuple3Mono).assertNext(objects -> {
Theme editModeTheme = objects.getT1();
Application forkedApp = objects.getT2();
Application srcApp = objects.getT3();
// same theme should be set to edit mode and published mode
assertThat(forkedApp.getEditModeThemeId()).isEqualTo(editModeTheme.getId());
assertThat(forkedApp.getPublishedModeThemeId()).isEqualTo(editModeTheme.getId());
// edit mode theme should be system theme
assertThat(editModeTheme.isSystemTheme()).isTrue();
// edit mode theme will have no application id and org id set as it's system theme
assertThat(editModeTheme.getOrganizationId()).isNullOrEmpty();
assertThat(editModeTheme.getApplicationId()).isNullOrEmpty();
// forked theme should be default theme
assertThat(editModeTheme.getName()).isEqualToIgnoringCase(Theme.DEFAULT_THEME_NAME);
// forked application should have same theme set
assertThat(srcApp.getEditModeThemeId()).isEqualTo(forkedApp.getEditModeThemeId());
}).verifyComplete();
}
@Test
@WithUserDetails("api_user")
public void forkApplicationToOrganization_WhenAppHasCustomSavedTheme_NewCustomThemeCreated() {
String uniqueString = UUID.randomUUID().toString();
Organization organization = new Organization();
organization.setName("org_" + uniqueString);
Mono<Tuple4<Theme, Theme, Application, Application>> tuple4Mono = organizationService.create(organization)
.flatMap(createdOrg -> {
Application application = new Application();
application.setName("app_" + uniqueString);
return applicationPageService.createApplication(application, createdOrg.getId());
}).flatMap(srcApplication -> {
Theme theme = new Theme();
theme.setName("theme_" + uniqueString);
return themeService.updateTheme(srcApplication.getId(), theme)
.then(themeService.persistCurrentTheme(srcApplication.getId(), theme))
.then(applicationService.findById(srcApplication.getId()));
}).flatMap(srcApplication -> {
Organization desOrg = new Organization();
desOrg.setName("org_dest_" + uniqueString);
return organizationService.create(desOrg).flatMap(createdOrg ->
applicationForkingService.forkApplicationToOrganization(srcApplication.getId(), createdOrg.getId())
).zipWith(Mono.just(srcApplication));
}).flatMap(applicationTuple2 -> {
Application forkedApp = applicationTuple2.getT1();
Application srcApp = applicationTuple2.getT2();
return Mono.zip(
themeService.getApplicationTheme(forkedApp.getId(), ApplicationMode.EDIT),
themeService.getApplicationTheme(forkedApp.getId(), ApplicationMode.PUBLISHED),
Mono.just(forkedApp),
Mono.just(srcApp)
);
});
StepVerifier.create(tuple4Mono).assertNext(objects -> {
Theme editModeTheme = objects.getT1();
Theme publishedModeTheme = objects.getT2();
Application forkedApp = objects.getT3();
Application srcApp = objects.getT4();
assertThat(forkedApp.getEditModeThemeId()).isEqualTo(editModeTheme.getId());
assertThat(forkedApp.getPublishedModeThemeId()).isEqualTo(publishedModeTheme.getId());
assertThat(forkedApp.getEditModeThemeId()).isNotEqualTo(forkedApp.getPublishedModeThemeId());
// published mode should have the custom theme as we publish after forking the app
assertThat(publishedModeTheme.isSystemTheme()).isFalse();
// published mode theme will have no application id and org id set as it's a copy
assertThat(publishedModeTheme.getOrganizationId()).isNullOrEmpty();
assertThat(publishedModeTheme.getApplicationId()).isNullOrEmpty();
// edit mode theme should be a custom one
assertThat(editModeTheme.isSystemTheme()).isFalse();
// edit mode theme will have application id and org id set as the customizations were saved
assertThat(editModeTheme.getOrganizationId()).isNullOrEmpty();
assertThat(editModeTheme.getApplicationId()).isNullOrEmpty();
// forked theme should have the same name as src theme
assertThat(editModeTheme.getName()).isEqualTo("theme_" + uniqueString);
assertThat(publishedModeTheme.getName()).isEqualTo("theme_" + uniqueString);
// forked application should have a new edit mode theme created, should not be same as src app theme
assertThat(srcApp.getEditModeThemeId()).isNotEqualTo(forkedApp.getEditModeThemeId());
assertThat(srcApp.getPublishedModeThemeId()).isNotEqualTo(forkedApp.getPublishedModeThemeId());
}).verifyComplete();
}
private Flux<ActionDTO> getActionsInOrganization(Organization organization) {
return applicationService
.findByOrganizationId(organization.getId(), READ_APPLICATIONS)

View File

@ -32,7 +32,6 @@ import com.appsmith.server.migrations.JsonSchemaVersions;
import com.appsmith.server.repositories.ApplicationRepository;
import com.appsmith.server.repositories.NewPageRepository;
import com.appsmith.server.repositories.PluginRepository;
import com.appsmith.server.repositories.ThemeRepository;
import com.appsmith.server.services.ActionCollectionService;
import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.DatasourceService;
@ -42,6 +41,7 @@ import com.appsmith.server.services.NewActionService;
import com.appsmith.server.services.NewPageService;
import com.appsmith.server.services.OrganizationService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.ThemeService;
import com.appsmith.server.services.UserService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
@ -90,6 +90,7 @@ import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES;
import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES;
import static com.appsmith.server.acl.AclPermission.MANAGE_THEMES;
import static com.appsmith.server.acl.AclPermission.READ_ACTIONS;
import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS;
import static com.appsmith.server.acl.AclPermission.READ_PAGES;
@ -148,7 +149,7 @@ public class ImportExportApplicationServiceTests {
PluginExecutorHelper pluginExecutorHelper;
@Autowired
ThemeRepository themeRepository;
ThemeService themeService;
private static final String INVALID_JSON_FILE = "invalid json file";
private static Plugin installedPlugin;
@ -859,8 +860,8 @@ public class ImportExportApplicationServiceTests {
.create(resultMono
.flatMap(application -> Mono.zip(
Mono.just(application),
themeRepository.findById(application.getEditModeThemeId()),
themeRepository.findById(application.getPublishedModeThemeId())
themeService.getThemeById(application.getEditModeThemeId(), MANAGE_THEMES),
themeService.getThemeById(application.getPublishedModeThemeId(), MANAGE_THEMES)
)))
.assertNext(tuple -> {
final Application application = tuple.getT1();
@ -869,9 +870,13 @@ public class ImportExportApplicationServiceTests {
assertThat(editTheme.isSystemTheme()).isFalse();
assertThat(editTheme.getName()).isEqualTo("Custom edit theme");
assertThat(editTheme.getOrganizationId()).isNull();
assertThat(editTheme.getApplicationId()).isNull();
assertThat(publishedTheme.isSystemTheme()).isFalse();
assertThat(publishedTheme.getName()).isEqualTo("Custom published theme");
assertThat(publishedTheme.getOrganizationId()).isNullOrEmpty();
assertThat(publishedTheme.getApplicationId()).isNullOrEmpty();
})
.verifyComplete();
}

View File

@ -632,11 +632,15 @@
"editModeTheme": {
"name": "Custom edit theme",
"new": true,
"isSystemTheme": false
"isSystemTheme": false,
"applicationId": "dummy-app-id",
"organizationId": "dummy-org-id"
},
"publishedTheme": {
"name": "Custom published theme",
"new": true,
"isSystemTheme": false
"isSystemTheme": false,
"applicationId": "dummy-app-id",
"organizationId": "dummy-org-id"
}
}