Merge pull request #30456 from appsmithorg/release

19/01 Daily Promotion
This commit is contained in:
Trisha Anand 2024-01-19 12:02:57 +05:30 committed by GitHub
commit e975f5eba0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
95 changed files with 8212 additions and 548 deletions

2
.github/config.json vendored

File diff suppressed because one or more lines are too long

View File

@ -403,10 +403,10 @@ jobs:
merged_upto_sha='${{ steps.merge.outputs.merged_upto_sha }}'
merged_upto_sha_short="$(echo "$merged_upto_sha" | grep -o '^.\{8\}' || true)"
details="🚨 TBP workflow failed in <$run_url|${{ github.run_id }}/attempts/${{ github.run_attempt }}>.
details="🚨 TBP workflow failed in <$run_url|${{ github.run_id }}/attempts/${{ github.run_attempt }}>."
# This unweildy horror of a sed command, converts standard Markdown links to Slack's unweildy link syntax.
slack_message="$(echo "$details" | sed -E 's/\[([^]]+)\]\(([^)]+)\)/<\2|\1>/g')"
# This unwieldy horror of a sed command, converts standard Markdown links to Slack's unwieldy link syntax.
slack_message=$(echo "$details" | sed -E 's/\[([^]]+)\]\(([^)]+)\)/<\2|\1>/g')
# This is the ChannelId of the tech channel.
body="$(jq -nc \

View File

@ -27,7 +27,8 @@ describe(
agHelper.Sleep(2000); // adding wait for app to load
homePage.LogOutviaAPI();
cy.generateUUID().then((uid) => {
cy.Signup(`${uid}@appsmithtest.com`, uid);
homePage.SignUp(`${uid}@appsmithtest.com`, uid);
onboarding.closeIntroModal();
});
homePage.NavigateToHome();
homePage.CreateNewApplication();

View File

@ -85,7 +85,10 @@ describe("", { tags: ["@tag.Widget", "@tag.Chart"] }, () => {
agHelper.AssertElementAbsence(
propPane._selectPropDropdown("x-axis label orientation"),
);
propPane.SelectPropertiesDropDown("Chart Type", "Custom Fusion Charts");
propPane.SelectPropertiesDropDown(
"Chart Type",
"Custom Fusion Charts (deprecated)",
);
agHelper.AssertElementAbsence(
propPane._selectPropDropdown("x-axis label orientation"),
);

View File

@ -10,9 +10,6 @@ describe(
{ tags: ["@tag.Widget", "@tag.Chart"] },
function () {
it("1. 3D EChart Custom Chart Widget Functionality", function () {
featureFlagIntercept({
release_custom_echarts_enabled: true,
});
_.agHelper.RefreshPage();
_.entityExplorer.DragDropWidgetNVerify(_.draggableWidgets.CHART);
@ -67,7 +64,10 @@ describe(
_.locators._widgetInDeployed(_.draggableWidgets.CHART),
);
_.propPane.SelectPropertiesDropDown("Chart type", "Custom Fusion Charts");
_.propPane.SelectPropertiesDropDown(
"Chart type",
"Custom Fusion Charts (deprecated)",
);
cy.wait(1000);
cy.get(publicWidgetsPage.chartWidget).matchImageSnapshot("FusionCharts");

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -108,7 +108,7 @@
"cypress-log-to-output": "^1.1.2",
"dayjs": "^1.10.6",
"deep-diff": "^1.0.2",
"design-system": "npm:@appsmithorg/design-system@2.1.30",
"design-system": "npm:@appsmithorg/design-system@2.1.31",
"design-system-old": "npm:@appsmithorg/design-system-old@1.1.14",
"downloadjs": "^1.4.7",
"echarts": "^5.4.2",

View File

@ -0,0 +1,72 @@
<svg width="56" height="52" viewBox="0 0 56 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="56" height="52" rx="3" fill="white"/>
<g clip-path="url(#clip0_9998_66064)">
<mask id="path-4-outside-1_9998_66064" maskUnits="userSpaceOnUse" x="3" y="4" width="14" height="12" fill="black">
<rect fill="white" x="3" y="4" width="14" height="12"/>
<path d="M4 4H17V15H4V4Z"/>
</mask>
<path d="M4 4H17V15H4V4Z" fill="#E3E8EF"/>
<path d="M4 15H3V16H4V15ZM17 14H4V16H17V14ZM5 15V4H3V15H5Z" fill="#CDD5DF" mask="url(#path-4-outside-1_9998_66064)"/>
<mask id="path-6-outside-2_9998_66064" maskUnits="userSpaceOnUse" x="16" y="4" width="13" height="12" fill="black">
<rect fill="white" x="16" y="4" width="13" height="12"/>
<path d="M17 4H29V15H17V4Z"/>
</mask>
<path d="M17 4H29V15H17V4Z" fill="#E3E8EF"/>
<path d="M17 15H16V16H17V15ZM29 14H17V16H29V14ZM18 15V4H16V15H18Z" fill="#CDD5DF" mask="url(#path-6-outside-2_9998_66064)"/>
<mask id="path-8-outside-3_9998_66064" maskUnits="userSpaceOnUse" x="28" y="4" width="24" height="12" fill="black">
<rect fill="white" x="28" y="4" width="24" height="12"/>
<path d="M29 4H52V15H29V4Z"/>
</mask>
<path d="M29 4H52V15H29V4Z" fill="#E3E8EF"/>
<path d="M29 15H28V16H29V15ZM52 14H29V16H52V14ZM30 15V4H28V15H30Z" fill="#CDD5DF" mask="url(#path-8-outside-3_9998_66064)"/>
<mask id="path-10-outside-4_9998_66064" maskUnits="userSpaceOnUse" x="3" y="15" width="14" height="12" fill="black">
<rect fill="white" x="3" y="15" width="14" height="12"/>
<path d="M4 15H17V26H4V15Z"/>
</mask>
<path d="M4 26H3V27H4V26ZM17 25H4V27H17V25ZM5 26V15H3V26H5Z" fill="#CDD5DF" mask="url(#path-10-outside-4_9998_66064)"/>
<mask id="path-12-outside-5_9998_66064" maskUnits="userSpaceOnUse" x="16" y="15" width="13" height="12" fill="black">
<rect fill="white" x="16" y="15" width="13" height="12"/>
<path d="M17 15H29V26H17V15Z"/>
</mask>
<path d="M17 26H16V27H17V26ZM29 25H17V27H29V25ZM18 26V15H16V26H18Z" fill="#CDD5DF" mask="url(#path-12-outside-5_9998_66064)"/>
<mask id="path-14-outside-6_9998_66064" maskUnits="userSpaceOnUse" x="28" y="15" width="24" height="12" fill="black">
<rect fill="white" x="28" y="15" width="24" height="12"/>
<path d="M29 15H52V26H29V15Z"/>
</mask>
<path d="M29 26H28V27H29V26ZM52 25H29V27H52V25ZM30 26V15H28V26H30Z" fill="#CDD5DF" mask="url(#path-14-outside-6_9998_66064)"/>
<mask id="path-16-outside-7_9998_66064" maskUnits="userSpaceOnUse" x="3" y="26" width="14" height="12" fill="black">
<rect fill="white" x="3" y="26" width="14" height="12"/>
<path d="M4 26H17V37H4V26Z"/>
</mask>
<path d="M4 37H3V38H4V37ZM17 36H4V38H17V36ZM5 37V26H3V37H5Z" fill="#CDD5DF" mask="url(#path-16-outside-7_9998_66064)"/>
<mask id="path-18-outside-8_9998_66064" maskUnits="userSpaceOnUse" x="16" y="26" width="13" height="12" fill="black">
<rect fill="white" x="16" y="26" width="13" height="12"/>
<path d="M17 26H28V37H17V26Z"/>
</mask>
<path d="M28 37V38H29V37H28ZM17 37H16V38H17V37ZM27 26V37H29V26H27ZM28 36H17V38H28V36ZM18 37V26H16V37H18Z" fill="#CDD5DF" mask="url(#path-18-outside-8_9998_66064)"/>
<mask id="path-20-outside-9_9998_66064" maskUnits="userSpaceOnUse" x="3" y="37" width="14" height="12" fill="black">
<rect fill="white" x="3" y="37" width="14" height="12"/>
<path d="M4 37H17V48H4V37Z"/>
</mask>
<path d="M4 48H3V49H4V48ZM17 47H4V49H17V47ZM5 48V37H3V48H5Z" fill="#CDD5DF" mask="url(#path-20-outside-9_9998_66064)"/>
<mask id="path-22-outside-10_9998_66064" maskUnits="userSpaceOnUse" x="16" y="37" width="13" height="12" fill="black">
<rect fill="white" x="16" y="37" width="13" height="12"/>
<path d="M17 37H28V48H17V37Z"/>
</mask>
<path d="M28 48V49H29V48H28ZM17 48H16V49H17V48ZM27 37V48H29V37H27ZM28 47H17V49H28V47ZM18 48V37H16V48H18Z" fill="#CDD5DF" mask="url(#path-22-outside-10_9998_66064)"/>
</g>
<rect x="4.5" y="4.5" width="47" height="43" rx="0.5" stroke="#CDD5DF"/>
<mask id="path-24-outside-11_9998_66064" maskUnits="userSpaceOnUse" x="29" y="27" width="25" height="23" fill="black">
<rect fill="white" x="29" y="27" width="25" height="23"/>
<path d="M29 27H51V47H29V27Z"/>
</mask>
<path d="M29 27H51V47H29V27Z" fill="white"/>
<path d="M51 47V50H54V47H51ZM48 27V47H54V27H48ZM51 44H29V50H51V44Z" fill="white" mask="url(#path-24-outside-11_9998_66064)"/>
<path d="M47.5 38.5C47.5 38.5 45.5 43.5 40.5 43.5C35.5 43.5 33.5 38.5 33.5 38.5C33.5 38.5 35.5 33.5 40.5 33.5C45.5 33.5 47.5 38.5 47.5 38.5Z" stroke="#CC3D00"/>
<circle cx="40.5" cy="38.5" r="3" stroke="#CC3D00"/>
<defs>
<clipPath id="clip0_9998_66064">
<rect x="4" y="4" width="48" height="44" rx="1" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,68 @@
<svg width="56" height="52" viewBox="0 0 56 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="56" height="52" rx="3" fill="white"/>
<g clip-path="url(#clip0_9998_66086)">
<mask id="path-4-outside-1_9998_66086" maskUnits="userSpaceOnUse" x="3" y="4" width="14" height="12" fill="black">
<rect fill="white" x="3" y="4" width="14" height="12"/>
<path d="M4 4H17V15H4V4Z"/>
</mask>
<path d="M4 4H17V15H4V4Z" fill="#E3E8EF"/>
<path d="M4 15H3V16H4V15ZM17 14H4V16H17V14ZM5 15V4H3V15H5Z" fill="#CDD5DF" mask="url(#path-4-outside-1_9998_66086)"/>
<mask id="path-6-outside-2_9998_66086" maskUnits="userSpaceOnUse" x="16" y="4" width="13" height="12" fill="black">
<rect fill="white" x="16" y="4" width="13" height="12"/>
<path d="M17 4H29V15H17V4Z"/>
</mask>
<path d="M17 4H29V15H17V4Z" fill="#E3E8EF"/>
<path d="M17 15H16V16H17V15ZM29 14H17V16H29V14ZM18 15V4H16V15H18Z" fill="#CDD5DF" mask="url(#path-6-outside-2_9998_66086)"/>
<mask id="path-8-outside-3_9998_66086" maskUnits="userSpaceOnUse" x="28" y="4" width="24" height="12" fill="black">
<rect fill="white" x="28" y="4" width="24" height="12"/>
<path d="M29 4H52V15H29V4Z"/>
</mask>
<path d="M29 4H52V15H29V4Z" fill="#E3E8EF"/>
<path d="M29 15H28V16H29V15ZM52 14H29V16H52V14ZM30 15V4H28V15H30Z" fill="#CDD5DF" mask="url(#path-8-outside-3_9998_66086)"/>
<mask id="path-10-outside-4_9998_66086" maskUnits="userSpaceOnUse" x="3" y="15" width="14" height="12" fill="black">
<rect fill="white" x="3" y="15" width="14" height="12"/>
<path d="M4 15H17V26H4V15Z"/>
</mask>
<path d="M4 26H3V27H4V26ZM17 25H4V27H17V25ZM5 26V15H3V26H5Z" fill="#CDD5DF" mask="url(#path-10-outside-4_9998_66086)"/>
<mask id="path-12-outside-5_9998_66086" maskUnits="userSpaceOnUse" x="16" y="15" width="13" height="12" fill="black">
<rect fill="white" x="16" y="15" width="13" height="12"/>
<path d="M17 15H29V26H17V15Z"/>
</mask>
<path d="M17 26H16V27H17V26ZM29 25H17V27H29V25ZM18 26V15H16V26H18Z" fill="#CDD5DF" mask="url(#path-12-outside-5_9998_66086)"/>
<mask id="path-14-outside-6_9998_66086" maskUnits="userSpaceOnUse" x="28" y="15" width="24" height="12" fill="black">
<rect fill="white" x="28" y="15" width="24" height="12"/>
<path d="M29 15H52V26H29V15Z"/>
</mask>
<path d="M29 26H28V27H29V26ZM52 25H29V27H52V25ZM30 26V15H28V26H30Z" fill="#CDD5DF" mask="url(#path-14-outside-6_9998_66086)"/>
<mask id="path-16-outside-7_9998_66086" maskUnits="userSpaceOnUse" x="3" y="26" width="14" height="12" fill="black">
<rect fill="white" x="3" y="26" width="14" height="12"/>
<path d="M4 26H17V37H4V26Z"/>
</mask>
<path d="M4 37H3V38H4V37ZM17 36H4V38H17V36ZM5 37V26H3V37H5Z" fill="#CDD5DF" mask="url(#path-16-outside-7_9998_66086)"/>
<mask id="path-18-outside-8_9998_66086" maskUnits="userSpaceOnUse" x="16" y="26" width="13" height="12" fill="black">
<rect fill="white" x="16" y="26" width="13" height="12"/>
<path d="M17 26H28V37H17V26Z"/>
</mask>
<path d="M28 37V38H29V37H28ZM17 37H16V38H17V37ZM27 26V37H29V26H27ZM28 36H17V38H28V36ZM18 37V26H16V37H18Z" fill="#CDD5DF" mask="url(#path-18-outside-8_9998_66086)"/>
<mask id="path-20-outside-9_9998_66086" maskUnits="userSpaceOnUse" x="3" y="37" width="14" height="12" fill="black">
<rect fill="white" x="3" y="37" width="14" height="12"/>
<path d="M4 37H17V48H4V37Z"/>
</mask>
<path d="M4 48H3V49H4V48ZM17 47H4V49H17V47ZM5 48V37H3V48H5Z" fill="#CDD5DF" mask="url(#path-20-outside-9_9998_66086)"/>
<mask id="path-22-outside-10_9998_66086" maskUnits="userSpaceOnUse" x="16" y="37" width="13" height="12" fill="black">
<rect fill="white" x="16" y="37" width="13" height="12"/>
<path d="M17 37H28V48H17V37Z"/>
</mask>
<path d="M28 48V49H29V48H28ZM17 48H16V49H17V48ZM27 37V48H29V37H27ZM28 47H17V49H28V47ZM18 48V37H16V48H18Z" fill="#CDD5DF" mask="url(#path-22-outside-10_9998_66086)"/>
</g>
<rect x="4.5" y="4.5" width="47" height="43" rx="0.5" stroke="#CDD5DF"/>
<path d="M51 47V50H54V47H51ZM48 27V47H54V27H48ZM51 44H29V50H51V44Z" fill="white" mask="url(#path-24-outside-11_9998_66086)"/>
<path d="M34.5 41.5V44.5H37.5L46.5 35.5L43.5 32.5L34.5 41.5Z" stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M41.5 34.5L44.5 37.5" stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M37.5 44.5H45.5" stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<clipPath id="clip0_9998_66086">
<rect x="4" y="4" width="48" height="44" rx="1" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,56 @@
<svg width="56" height="52" viewBox="0 0 56 52" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="56" height="52" rx="3" fill="white"/>
<rect x="4.5" y="4.5" width="31" height="6" rx="0.5" fill="white" stroke="#CDD5DF"/>
<rect x="37.5" y="4.5" width="14" height="6" rx="0.5" fill="#FFEEE5" stroke="#CC3D00"/>
<g clip-path="url(#clip0_9998_66109)">
<mask id="path-6-outside-1_9998_66109" maskUnits="userSpaceOnUse" x="3" y="15" width="14" height="12" fill="black">
<rect fill="white" x="3" y="15" width="14" height="12"/>
<path d="M4 15H17V26H4V15Z"/>
</mask>
<path d="M4 15H17V26H4V15Z" fill="#E3E8EF"/>
<path d="M4 26H3V27H4V26ZM17 25H4V27H17V25ZM5 26V15H3V26H5Z" fill="#CDD5DF" mask="url(#path-6-outside-1_9998_66109)"/>
<mask id="path-8-outside-2_9998_66109" maskUnits="userSpaceOnUse" x="16" y="15" width="13" height="12" fill="black">
<rect fill="white" x="16" y="15" width="13" height="12"/>
<path d="M17 15H29V26H17V15Z"/>
</mask>
<path d="M17 15H29V26H17V15Z" fill="#E3E8EF"/>
<path d="M17 26H16V27H17V26ZM29 25H17V27H29V25ZM18 26V15H16V26H18Z" fill="#CDD5DF" mask="url(#path-8-outside-2_9998_66109)"/>
<mask id="path-10-outside-3_9998_66109" maskUnits="userSpaceOnUse" x="28" y="15" width="24" height="12" fill="black">
<rect fill="white" x="28" y="15" width="24" height="12"/>
<path d="M29 15H52V26H29V15Z"/>
</mask>
<path d="M29 15H52V26H29V15Z" fill="#E3E8EF"/>
<path d="M29 26H28V27H29V26ZM52 25H29V27H52V25ZM30 26V15H28V26H30Z" fill="#CDD5DF" mask="url(#path-10-outside-3_9998_66109)"/>
<mask id="path-12-outside-4_9998_66109" maskUnits="userSpaceOnUse" x="3" y="26" width="14" height="12" fill="black">
<rect fill="white" x="3" y="26" width="14" height="12"/>
<path d="M4 26H17V37H4V26Z"/>
</mask>
<path d="M4 37H3V38H4V37ZM17 36H4V38H17V36ZM5 37V26H3V37H5Z" fill="#CDD5DF" mask="url(#path-12-outside-4_9998_66109)"/>
<mask id="path-14-outside-5_9998_66109" maskUnits="userSpaceOnUse" x="16" y="26" width="13" height="12" fill="black">
<rect fill="white" x="16" y="26" width="13" height="12"/>
<path d="M17 26H28V37H17V26Z"/>
</mask>
<path d="M28 37V38H29V37H28ZM17 37H16V38H17V37ZM27 26V37H29V26H27ZM28 36H17V38H28V36ZM18 37V26H16V37H18Z" fill="#CDD5DF" mask="url(#path-14-outside-5_9998_66109)"/>
<mask id="path-16-outside-6_9998_66109" maskUnits="userSpaceOnUse" x="3" y="37" width="14" height="12" fill="black">
<rect fill="white" x="3" y="37" width="14" height="12"/>
<path d="M4 37H17V48H4V37Z"/>
</mask>
<path d="M4 48H3V49H4V48ZM17 47H4V49H17V47ZM5 48V37H3V48H5Z" fill="#CDD5DF" mask="url(#path-16-outside-6_9998_66109)"/>
<mask id="path-18-outside-7_9998_66109" maskUnits="userSpaceOnUse" x="16" y="37" width="13" height="12" fill="black">
<rect fill="white" x="16" y="37" width="13" height="12"/>
<path d="M17 37H28V48H17V37Z"/>
</mask>
<path d="M28 48V49H29V48H28ZM17 48H16V49H17V48ZM27 37V48H29V37H27ZM28 47H17V49H28V47ZM18 48V37H16V48H18Z" fill="#CDD5DF" mask="url(#path-18-outside-7_9998_66109)"/>
</g>
<rect x="4.5" y="15.5" width="47" height="32" rx="0.5" stroke="#CDD5DF"/>
<path d="M51 47V50H54V47H51ZM48 27V47H54V27H48ZM51 44H29V50H51V44Z" fill="white" mask="url(#path-20-outside-8_9998_66109)"/>
<path d="M37.5 45V36.5" stroke="#CC3D00" stroke-linecap="round"/>
<path d="M35.5 43.5L37.5 45.5L39.5 43.5" stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M43.5 41.5V33" stroke="#CC3D00" stroke-linecap="round"/>
<path d="M41.5 34.5L43.5 32.5L45.5 34.5" stroke="#CC3D00" stroke-linecap="round" stroke-linejoin="round"/>
<defs>
<clipPath id="clip0_9998_66109">
<rect x="4" y="15" width="48" height="33" rx="1" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -1,35 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="4" fill="#FAEAE8"/>
<rect x="6" y="5" width="16.725" height="10.2208" rx="2" fill="white" stroke="#FFB4B4" stroke-width="2"/>
<g clip-path="url(#clip0_589_3613)">
<rect x="6" y="5" width="16.725" height="10.2208" fill="white"/>
<path d="M6.46448 14.7563L12.9686 10.1104L17.1499 11.5042L22.7249 5.9292" stroke="#E15615" stroke-width="1.85833"/>
<path d="M12.9686 10.1104L6.46448 14.7563V15.2209H22.7249V5.9292L17.1499 11.5042L12.9686 10.1104Z" fill="#FAEAE8"/>
</g>
<rect x="25.6" y="5" width="16.3636" height="10" rx="2" fill="white" stroke="#FFB4B4" stroke-width="2"/>
<g clip-path="url(#clip1_589_3613)">
<rect x="25.6" y="5" width="16.3636" height="10" fill="white"/>
<path d="M27.8728 13.6363C27.8728 13.1342 28.2798 12.7272 28.7819 12.7272H29.691C30.1931 12.7272 30.6001 13.1342 30.6001 13.6363V15.4544H27.8728V13.6363Z" fill="#FAEAE8"/>
<path d="M32.4183 10.909C32.4183 10.4069 32.8253 9.99989 33.3273 9.99989H34.2364C34.7385 9.99989 35.1455 10.4069 35.1455 10.909V15.4544H32.4183V10.909Z" fill="#FAEAE8"/>
<path d="M36.9637 7.27262C36.9637 6.77054 37.3707 6.36353 37.8728 6.36353H38.7819C39.284 6.36353 39.691 6.77054 39.691 7.27262V15.4544H36.9637V7.27262Z" fill="#E15615"/>
</g>
<rect x="6" y="19.5" width="36" height="23.5" rx="2" fill="white" stroke="#FFB4B4" stroke-width="2"/>
<g clip-path="url(#clip2_589_3613)">
<rect x="6" y="19" width="36" height="25" fill="white"/>
<rect x="8" y="21.5" width="12" height="3" rx="1" fill="#E15615"/>
<rect x="8" y="27" width="32" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="32.5" width="32" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="38" width="32" height="3" rx="1" fill="#FAEAE8"/>
</g>
<defs>
<clipPath id="clip0_589_3613">
<rect x="6" y="5" width="16.725" height="10.2208" rx="1.85833" fill="white"/>
</clipPath>
<clipPath id="clip1_589_3613">
<rect x="25.6" y="5" width="16.3636" height="10" rx="1.81818" fill="white"/>
</clipPath>
<clipPath id="clip2_589_3613">
<rect x="6" y="19.5" width="36" height="23.5" rx="2" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -1,19 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="4" fill="#FAEAE8"/>
<rect x="6" y="5" width="36" height="38" rx="2" fill="white" stroke="#FFB4B4" stroke-width="2"/>
<g clip-path="url(#clip0_589_3597)">
<rect x="6" y="5" width="37" height="38" fill="white"/>
<rect x="8" y="8" width="7" height="2" rx="1" fill="#FAEAE8"/>
<rect x="8" y="19" width="32" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="13" width="15" height="3" rx="1" fill="#FAEAE8"/>
<rect x="25" y="13" width="15" height="3" rx="1" fill="#FAEAE8"/>
<rect x="29" y="37" width="11" height="3" rx="1" fill="#E15615"/>
<rect x="8" y="37" width="11" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="25" width="32" height="9" rx="1" fill="#FAEAE8"/>
</g>
<defs>
<clipPath id="clip0_589_3597">
<rect x="6" y="5" width="36" height="38" rx="2" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 927 B

View File

@ -1,22 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="4" fill="#FAEAE8"/>
<rect x="6" y="5" width="14" height="38" rx="2" fill="white" stroke="#FFB4B4" stroke-width="2"/>
<g clip-path="url(#clip0_589_3637)">
<rect x="6" y="5" width="14" height="38" fill="white"/>
<rect x="8" y="8" width="6" height="2" rx="1" fill="#FAEAE8"/>
<rect x="8" y="13" width="6" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="31" width="6" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="19" width="10" height="3" rx="1" fill="#E15615"/>
<rect x="8" y="37" width="6" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="25" width="6" height="3" rx="1" fill="#FAEAE8"/>
</g>
<rect x="23" y="5" width="19" height="38" rx="2" fill="#E15615"/>
<path d="M29.5 24C29.5 24.6904 28.9404 25.25 28.25 25.25C27.5596 25.25 27 24.6904 27 24C27 23.3096 27.5596 22.75 28.25 22.75C28.9404 22.75 29.5 23.3096 29.5 24Z" fill="white"/>
<path d="M33.75 24C33.75 24.6904 33.1904 25.25 32.5 25.25C31.8096 25.25 31.25 24.6904 31.25 24C31.25 23.3096 31.8096 22.75 32.5 22.75C33.1904 22.75 33.75 23.3096 33.75 24Z" fill="white"/>
<path d="M38 24C38 24.6904 37.4404 25.25 36.75 25.25C36.0596 25.25 35.5 24.6904 35.5 24C35.5 23.3096 36.0596 22.75 36.75 22.75C37.4404 22.75 38 23.3096 38 24Z" fill="white"/>
<defs>
<clipPath id="clip0_589_3637">
<rect x="6" y="5" width="14" height="38" rx="2" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -1,20 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="48" height="48" rx="4" fill="#FAEAE8"/>
<rect x="6" y="5" width="14" height="38" rx="2" fill="white" stroke="#FFB4B4" stroke-width="2"/>
<g clip-path="url(#clip0_589_3654)">
<rect x="6" y="5" width="14" height="38" fill="white"/>
<rect x="8" y="8" width="6" height="2" rx="1" fill="#FAEAE8"/>
<rect x="8" y="13" width="6" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="31" width="6" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="19" width="10" height="3" rx="1" fill="#E15615"/>
<rect x="8" y="37" width="6" height="3" rx="1" fill="#FAEAE8"/>
<rect x="8" y="25" width="6" height="3" rx="1" fill="#FAEAE8"/>
</g>
<rect x="23" y="5" width="19" height="38" rx="2" fill="#E15615"/>
<path d="M28.2222 28.7758H29.0863L34.7782 23.0746L33.9141 22.2091L28.2222 27.9103V28.7758ZM38 30H27V27.4028L35.2103 19.1792C35.3249 19.0645 35.4803 19 35.6423 19C35.8044 19 35.9598 19.0645 36.0744 19.1792L37.8032 20.9109C37.9178 21.0256 37.9821 21.1813 37.9821 21.3436C37.9821 21.5059 37.9178 21.6616 37.8032 21.7764L30.8152 28.7758H38V30ZM34.7782 21.3436L35.6423 22.2091L36.5064 21.3436L35.6423 20.4781L34.7782 21.3436Z" fill="white"/>
<defs>
<clipPath id="clip0_589_3654">
<rect x="6" y="5" width="14" height="38" rx="2" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -2229,9 +2229,9 @@ export const DATASOURCE_BLANK_STATE_MESSAGE = () => "No datasources to display";
export const STARTER_TEMPLATE_PAGE_LAYOUTS = {
header: () => "Choose a template",
layouts: {
dashboard: {
name: () => "Visualize your data",
description: () => "Use to see your data in charts",
sortFilterTable: {
name: () => "Filter your data",
description: () => "Use to filter and sort your data",
},
form: {
name: () => "Form",
@ -2356,6 +2356,7 @@ export const CUSTOM_WIDGET_FEATURE = {
},
templateKey: {
blank: () => "Blank",
vanillaJs: () => "Vanilla JS",
react: () => "React",
vue: () => "Vue",
},
@ -2411,6 +2412,8 @@ export const CUSTOM_WIDGET_FEATURE = {
helpDropdown: {
stackoverflow: () => "Search StackOverflow",
},
noOnReadyWarning: (url: string) =>
`Missing appsmith.onReady() function call. Initiate your component inside 'appsmith.onReady()' for your custom widget to work as expected. For more information - ${url}`,
},
preview: {
eventFired: () => "Event fired:",

View File

@ -10,14 +10,11 @@ export const FEATURE_FLAG = {
ab_wds_enabled: "ab_wds_enabled",
release_table_serverside_filtering_enabled:
"release_table_serverside_filtering_enabled",
release_custom_echarts_enabled: "release_custom_echarts_enabled",
license_branding_enabled: "license_branding_enabled",
release_git_status_lite_enabled: "release_git_status_lite_enabled",
license_sso_saml_enabled: "license_sso_saml_enabled",
license_sso_oidc_enabled: "license_sso_oidc_enabled",
release_git_connect_v2_enabled: "release_git_connect_v2_enabled",
deprecate_custom_fusioncharts_enabled:
"deprecate_custom_fusioncharts_enabled",
license_private_embeds_enabled: "license_private_embeds_enabled",
release_show_publish_app_to_community_enabled:
"release_show_publish_app_to_community_enabled",
@ -60,13 +57,11 @@ export const DEFAULT_FEATURE_FLAG_VALUE: FeatureFlags = {
release_embed_hide_share_settings_enabled: false,
ab_wds_enabled: false,
release_table_serverside_filtering_enabled: false,
release_custom_echarts_enabled: false,
license_branding_enabled: false,
release_git_status_lite_enabled: false,
license_sso_saml_enabled: false,
license_sso_oidc_enabled: false,
release_git_connect_v2_enabled: false,
deprecate_custom_fusioncharts_enabled: false,
license_private_embeds_enabled: false,
release_show_publish_app_to_community_enabled: false,
license_gac_enabled: false,

View File

@ -1,28 +1,16 @@
import { groupBy, sortBy } from "lodash";
import { createSelector } from "reselect";
import { PluginType } from "entities/Action";
import type { EntityItem } from "@appsmith/selectors/entitiesSelector";
import {
isEmbeddedAIDataSource,
isEmbeddedRestDatasource,
} from "entities/Datasource";
import {
getCurrentActions,
getCurrentJSCollections,
selectDatasourceIdToNameMap,
} from "./entitiesSelector";
getJSSegmentItems,
getQuerySegmentItems,
} from "@appsmith/selectors/entitiesSelector";
export type EditorSegmentList = Array<{
group: string | "NA";
items: EntityItem[];
}>;
export interface EntityItem {
title: string;
type: PluginType;
key: string;
group?: string;
}
const groupAndSortEntitySegmentList = (
items: EntityItem[],
): EditorSegmentList => {
@ -48,50 +36,12 @@ function recentSortEntitySegmentTabs(items: EntityItem[]) {
return sortBy(items, "title");
}
export const getQuerySegmentItems = createSelector(
getCurrentActions,
selectDatasourceIdToNameMap,
(actions, datasourceIdToNameMap) => {
const items: EntityItem[] = actions.map((action) => {
let group;
if (action.config.pluginType === PluginType.API) {
group = isEmbeddedRestDatasource(action.config.datasource)
? "APIs"
: datasourceIdToNameMap[action.config.datasource.id] ?? "APIs";
} else if (action.config.pluginType === PluginType.AI) {
group = isEmbeddedAIDataSource(action.config.datasource)
? "AI Queries"
: datasourceIdToNameMap[action.config.datasource.id] ?? "AI Queries";
} else {
group = datasourceIdToNameMap[action.config.datasource.id];
}
return {
title: action.config.name,
key: action.config.id,
type: action.config.pluginType,
group,
};
});
return items;
},
);
export const selectQuerySegmentEditorList = createSelector(
getQuerySegmentItems,
(items) => {
return groupAndSortEntitySegmentList(items);
},
);
export const getJSSegmentItems = createSelector(
getCurrentJSCollections,
(jsActions) => {
const items: EntityItem[] = jsActions.map((js) => ({
title: js.config.name,
key: js.config.id,
type: PluginType.JS,
}));
return items;
},
);
export const selectJSSegmentEditorList = createSelector(
getJSSegmentItems,
(items) => {

View File

@ -1458,3 +1458,49 @@ export const getNewEntityName = createSelector(
return getNextEntityName(prefix, actionNames.concat(jsActionNames));
},
);
export interface EntityItem {
title: string;
type: PluginType;
key: string;
group?: string;
}
export const getQuerySegmentItems = createSelector(
getCurrentActions,
selectDatasourceIdToNameMap,
(actions, datasourceIdToNameMap) => {
const items: EntityItem[] = actions.map((action) => {
let group;
if (action.config.pluginType === PluginType.API) {
group = isEmbeddedRestDatasource(action.config.datasource)
? "APIs"
: datasourceIdToNameMap[action.config.datasource.id] ?? "APIs";
} else if (action.config.pluginType === PluginType.AI) {
group = isEmbeddedAIDataSource(action.config.datasource)
? "AI Queries"
: datasourceIdToNameMap[action.config.datasource.id] ?? "AI Queries";
} else {
group = datasourceIdToNameMap[action.config.datasource.id];
}
return {
title: action.config.name,
key: action.config.id,
type: action.config.pluginType,
group,
};
});
return items;
},
);
export const getJSSegmentItems = createSelector(
getCurrentJSCollections,
(jsActions) => {
const items: EntityItem[] = jsActions.map((js) => ({
title: js.config.name,
key: js.config.id,
type: PluginType.JS,
}));
return items;
},
);

View File

@ -432,6 +432,7 @@ export type VERSION_UPDATE_EVENTS =
| "VERSION_UPDATED_FAILED";
export type CUSTOM_WIDGET_EVENTS =
| "CUSTOM_WIDGET_LOAD_INIT"
| "CUSTOM_WIDGET_EDIT_SOURCE_CLICKED"
| "CUSTOM_WIDGET_ADD_EVENT_CLICKED"
| "CUSTOM_WIDGET_ADD_EVENT_CANCEL_CLICKED"
@ -450,4 +451,6 @@ export type CUSTOM_WIDGET_EVENTS =
| "CUSTOM_WIDGET_BUILDER_REFERENCE_VISIBILITY_CHANGED"
| "CUSTOM_WIDGET_BUILDER_REFERENCE_EVENT_OPENED"
| "CUSTOM_WIDGET_BUILDER_DEBUGGER_CLEARED"
| "CUSTOM_WIDGET_BUILDER_DEBUGGER_VISIBILITY_CHANGED";
| "CUSTOM_WIDGET_BUILDER_DEBUGGER_VISIBILITY_CHANGED"
| "CUSTOM_WIDGET_API_TRIGGER_EVENT"
| "CUSTOM_WIDGET_API_UPDATE_MODEL";

View File

@ -604,7 +604,7 @@ export function getJSOptions(
const jsFunction = {
label: js.name,
id: js.id,
value: jsModuleInstance.config.name + "." + js.name,
value: jsModuleInstance.name + "." + js.name,
type: jsOption.value,
icon: <Icon name="js-function" size="md" />,
args: argValue,

View File

@ -12,15 +12,15 @@ export const COMMUNITY_PORTAL = {
const RecordEdit = importSvg(
async () =>
import("../assets/icons/templates/starter-template-record-edit.svg"),
import("../assets/icons/templates/canvas-starter-record-edit.svg"),
);
const RecordDetails = importSvg(
async () =>
import("../assets/icons/templates/starter-template-record-details.svg"),
import("../assets/icons/templates/canvas-starter-record-details.svg"),
);
const Dashboard = importSvg(
const SortFilterTable = importSvg(
async () =>
import("../assets/icons/templates/starter-template-dashboard.svg"),
import("../assets/icons/templates/canvas-starter-sort-filter-table.svg"),
);
export const STARTER_BUILDING_BLOCK_TEMPLATE_NAME = "Starter Building Block";
@ -61,17 +61,17 @@ export const STARTER_BUILDING_BLOCKS = {
{
id: 3,
title: createMessage(
STARTER_BUILDING_BLOCKS_LAYOUTS.layouts.dashboard.name,
STARTER_BUILDING_BLOCKS_LAYOUTS.layouts.sortFilterTable.name,
),
description: createMessage(
STARTER_BUILDING_BLOCKS_LAYOUTS.layouts.dashboard.description,
STARTER_BUILDING_BLOCKS_LAYOUTS.layouts.sortFilterTable.description,
),
icon: <Dashboard />,
icon: <SortFilterTable />,
screenshot:
"https://s3.us-east-2.amazonaws.com/template.appsmith.com/canvas-starter-page-layout-dashboard.png",
"https://images.ctfassets.net/lpvian6u6i39/55ERsTeUvbAzJVaBBsInZr/0009fee0adb710b91c18a5bdc989deeb/canvas-starter-building-block-sort-filter-table.png?fm=png&q=50",
templateId: "6530e343fa63b553e4be0266",
templateName: STARTER_BUILDING_BLOCK_TEMPLATE_NAME,
templatePageName: "Dashboard",
templatePageName: "Sort and Filter Table",
},
],
};

View File

@ -512,13 +512,8 @@ describe("getAllPathsFromPropertyConfig", () => {
],
setAdaptiveYMin: "0",
};
const customEChartEnabled = true;
const showFusionChartDeprecationMessage = true;
const config = [
...contentConfig(customEChartEnabled, showFusionChartDeprecationMessage),
...styleConfig,
];
const config = [...contentConfig(), ...styleConfig];
const bindingPaths = {
chartType: EvaluationSubstitutionType.TEMPLATE,

View File

@ -1,5 +1,4 @@
import styled from "styled-components";
import { Text } from "design-system";
import { Colors } from "constants/Colors";
@ -49,35 +48,6 @@ export const TemplateLayoutContainer = styled.div`
}
`;
export const TemplateLayoutHeaderText = styled(Text)<{ layoutActive: boolean }>`
font-size: 16px;
font-weight: 600;
line-height: 24px;
margin-bottom: 16px;
color: var(--colors-semantics-text-emphasis);
opacity: ${(props) => (props.layoutActive ? "1" : "0.7")};
`;
export const TemplateLayoutRowItemTitle = styled.p<{ layoutActive: boolean }>`
font-size: 14px;
line-height: 20px;
text-align: center;
font-weight: 500;
color: var(--colors-ui-content-heading-sub-section-heading);
opacity: ${(props) => (props.layoutActive ? "1" : "0.7")};
`;
export const TemplateLayoutRowItemDescription = styled.p<{
layoutActive: boolean;
}>`
font-size: 12px;
line-height: 16px;
text-align: center;
font-weight: 400;
color: var(--colors-ui-content-supplementary);
opacity: ${(props) => (props.layoutActive ? "1" : "0.7")};
`;
export const TemplateLayoutContentGrid = styled.div`
display: flex;
justify-content: center;
@ -116,7 +86,8 @@ export const TemplateLayoutContentItemContent = styled.div`
export const IconContainer = styled.div<{ layoutItemActive: boolean }>`
border-width: 1px;
border-radius: 4px;
margin-bottom: 8px;
margin-bottom: 16px;
margin-top: 8px;
border-color: ${(props) =>
props.layoutItemActive ? Colors.PRIMARY_ORANGE : "transparent"};
`;

View File

@ -15,7 +15,7 @@ import {
STARTER_BUILDING_BLOCKS,
STARTER_BUILDING_BLOCK_TEMPLATE_NAME,
} from "constants/TemplatesConstants";
import { Button } from "design-system";
import { Button, Text } from "design-system";
import LoadingScreen from "pages/Templates/TemplatesModal/LoadingScreen";
import { useDispatch, useSelector } from "react-redux";
import {
@ -31,9 +31,6 @@ import {
TemplateLayoutContentItem,
TemplateLayoutContentItemContent,
TemplateLayoutFrame,
TemplateLayoutHeaderText,
TemplateLayoutRowItemDescription,
TemplateLayoutRowItemTitle,
} from "./StyledComponents";
function StarterBuildingBlocks() {
@ -128,9 +125,16 @@ function StarterBuildingBlocks() {
onMouseEnter={() => setLayoutActive(true)}
onMouseLeave={() => setLayoutActive(false)}
>
<TemplateLayoutHeaderText layoutActive={layoutActive}>
<Text
kind="heading-m"
style={{
opacity: layoutActive ? "1" : "0.7",
color: "var(--colors-semantics-text-emphasis)",
marginBottom: "16px",
}}
>
{createMessage(STARTER_TEMPLATE_PAGE_LAYOUTS.header)}
</TemplateLayoutHeaderText>
</Text>
<TemplateLayoutContentGrid>
{layoutItems.map((item, index) => (
@ -155,12 +159,27 @@ function StarterBuildingBlocks() {
</IconContainer>
<TemplateLayoutContentItemContent>
<TemplateLayoutRowItemTitle layoutActive={layoutActive}>
<Text
kind={"heading-xs"}
style={{
color:
"var(--colors-ui-content-heading-sub-section-heading)",
opacity: layoutActive ? "1" : "0.7",
fontWeight: 500,
}}
>
{item.title}
</TemplateLayoutRowItemTitle>
<TemplateLayoutRowItemDescription layoutActive={layoutActive}>
</Text>
<Text
kind="body-s"
style={{
color: "var(--colors-ui-content-supplementary)",
opacity: layoutActive ? "1" : "0.7",
textAlign: "center",
}}
>
{item.description}
</TemplateLayoutRowItemDescription>
</Text>
</TemplateLayoutContentItemContent>
</TemplateLayoutContentItem>
))}

View File

@ -1,13 +0,0 @@
import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
export default {
key: createMessage(CUSTOM_WIDGET_FEATURE.templateKey.blank),
uncompiledSrcDoc: {
html: "",
css: "",
js: "",
},
};

View File

@ -1,5 +1,5 @@
import blank from "./blank";
import vanillaJs from "./vanillaJs";
import react from "./react";
import vue from "./vue";
export default [blank, react, vue];
export default [vanillaJs, react, vue];

View File

@ -2,6 +2,7 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
export default {
key: createMessage(CUSTOM_WIDGET_FEATURE.templateKey.react),
@ -43,6 +44,7 @@ export default {
.button-container button {
margin: 0 10px;
border-radius: var(--appsmith-theme-borderRadius);
}
.button-container button.primary {
@ -58,10 +60,6 @@ import reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm'
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm'
import Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm'
const style = {
maxWidth: "400px",
}
function App() {
const [currentIndex, setCurrentIndex] = React.useState(0);
@ -75,7 +73,7 @@ function App() {
};
return (
<Card className="app" style={style}>
<Card className="app">
<div className="tip-container">
<div className="tip-header">
<h2>Custom Widget</h2>
@ -92,92 +90,12 @@ function App() {
}
appsmith.onReady(() => {
/*
* This handler function will get called when parent application is ready.
* Initialize your component here
* more info - ${CUSTOM_WIDGET_ONREADY_DOC_URL}
*/
reactDom.render(<App />, document.getElementById("root"));
});`,
},
srcDoc: {
html: `<!-- no need to write html, head, body tags, it is handled by the widget -->
<div id="root"></div>
`,
css: `.app {
height: calc(var(--appsmith-ui-height) * 1px);
width: calc(var(--appsmith-ui-width) * 1px);
justify-content: center;
border-radius: var(--appsmith-theme-borderRadius);
box-shadow: var(--appsmith-theme-boxShadow);
}
.tip-container {
margin-bottom: 20px;
}
.tip-container h2 {
margin-bottom: 20px;
font-size: 16px;
font-weight: 700;
}
.tip-header {
display: flex;
justify-content: space-between;
align-items: baseline;
}
.tip-header div {
color: #999;
}
.button-container {
text-align: right;
}
.button-container button {
margin: 0 10px;
}
.button-container button.primary {
background: var(--appsmith-theme-primaryColor) !important;
}
.button-container button.reset {
color: var(--appsmith-theme-primaryColor) !important;
border-color: var(--appsmith-theme-primaryColor) !important;
}`,
js: `import React from 'https://cdn.jsdelivr.net/npm/react@18.2.0/+esm';
import reactDom from 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm';
import { Button, Card } from 'https://cdn.jsdelivr.net/npm/antd@5.11.1/+esm';
import Markdown from 'https://cdn.jsdelivr.net/npm/react-markdown@9.0.1/+esm';
const style = {
maxWidth: "400px"
};
function App() {
const [currentIndex, setCurrentIndex] = React.useState(0);
const handleNext = () => {
setCurrentIndex(prevIndex => (prevIndex + 1) % appsmith.model.tips.length);
};
const handleReset = () => {
setCurrentIndex(0);
appsmith.triggerEvent("onReset");
};
return /*#__PURE__*/React.createElement(Card, {
className: "app",
style: style
}, /*#__PURE__*/React.createElement("div", {
className: "tip-container"
}, /*#__PURE__*/React.createElement("div", {
className: "tip-header"
}, /*#__PURE__*/React.createElement("h2", null, "Custom Widget"), /*#__PURE__*/React.createElement("div", null, currentIndex + 1, " / ", appsmith.model.tips.length, " ")), /*#__PURE__*/React.createElement(Markdown, null, appsmith.model.tips[currentIndex])), /*#__PURE__*/React.createElement("div", {
className: "button-container"
}, /*#__PURE__*/React.createElement(Button, {
className: "primary",
onClick: handleNext,
type: "primary"
}, "Next Tip"), /*#__PURE__*/React.createElement(Button, {
onClick: handleReset
}, "Reset")));
}
appsmith.onReady(() => {
reactDom.render( /*#__PURE__*/React.createElement(App, null), document.getElementById("root"));
});`,
},
};

View File

@ -0,0 +1,129 @@
import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
export default {
key: createMessage(CUSTOM_WIDGET_FEATURE.templateKey.vanillaJs),
uncompiledSrcDoc: {
html: `<div class="app">
<div class="tip-container">
<div class="tip-header">
<h2>Custom Widget</h2>
<div id="index"></div>
</div>
<div id="tip"></div>
</div>
<div class="button-container">
<button id="next">Next Tip</button>
<button id="reset">Reset</button>
</div>
</div>`,
css: `.app {
height: calc(var(--appsmith-ui-height) * 1px);
width: calc(var(--appsmith-ui-width) * 1px);
justify-content: center;
border-radius: var(--appsmith-theme-borderRadius);
box-shadow: var(--appsmith-theme-boxShadow);
padding: 29px 25px;
box-sizing: border-box;
font-family: system-ui;
background: #fff;
}
.tip-container {
margin-bottom: 20px;
font-size: 14px;
line-height: 1.571429;
}
.tip-container h2 {
margin-bottom: 20px;
font-size: 16px;
font-weight: 700;
}
.tip-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 9px;
}
.tip-header div {
color: #999;
}
.button-container {
text-align: right;
padding-top: 4px;
}
.button-container button {
margin: 0 10px;
cursor: pointer;
border-radius: var(--appsmith-theme-borderRadius);
padding: 6px 16px;
background: none;
}
.button-container button#next {
background: var(--appsmith-theme-primaryColor) !important;
color: #fff;
border:1px solid var(--appsmith-theme-primaryColor) !important;
}
.button-container button#reset {
border: 1px solid #999;
color: #999;
outline: none;
box-shadow: none;
}
.button-container button#reset:hover:not(:disabled) {
color: var(--appsmith-theme-primaryColor);
border-color: var(--appsmith-theme-primaryColor);
}
.button-container button#reset:disabled {
cursor: default;
}`,
js: `function initApp() {
const index = document.getElementById("index");
const tip = document.getElementById("tip");
const next = document.getElementById("next");
const reset = document.getElementById("reset");
let currentIndex = 0;
const updateDom = () => {
tip.innerHTML = appsmith.model.tips[currentIndex];
index.innerHTML = (currentIndex + 1) + " / " + appsmith.model.tips.length;
reset.disabled = currentIndex === 0;
};
next.addEventListener("click", () => {
currentIndex = (currentIndex + 1) % appsmith.model.tips.length;
updateDom();
});
reset.addEventListener("click", () => {
currentIndex = 0;
updateDom();
appsmith.triggerEvent("onReset");
});
updateDom();
}
appsmith.onReady(() => {
/*
* This handler function will get called when parent application is ready.
* Initialize your component here
* more info - ${CUSTOM_WIDGET_ONREADY_DOC_URL}
*/
initApp();
});`,
},
};

View File

@ -2,31 +2,118 @@ import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
export default {
key: createMessage(CUSTOM_WIDGET_FEATURE.templateKey.vue),
uncompiledSrcDoc: {
html: `<div id="hello-world-app">
<h1>{{ msg }}</h1>
html: `<div id="app">
<div class="tip-container">
<div class="tip-header">
<h2>Custom Widget</h2>
<div id="index">{{ currentIndex + 1 }} / {{ tips.length }}</div>
</div>
<div id="tip">{{ tips[currentIndex] }}</div>
</div>
<div class="button-container">
<button @click="next" id="next">Next Tip</button>
<button @click="reset" id="reset">Reset</button>
</div>
</div>
<script
src="//cdnjs.cloudflare.com/ajax/libs/vue/2.1.6/vue.min.js">
</script>`,
css: `#hello-world-app {
font-family: monospace;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.1.6/vue.min.js"></script>`,
css: `#app {
height: calc(var(--appsmith-ui-height) * 1px);
width: calc(var(--appsmith-ui-width) * 1px);
justify-content: center;
border-radius: var(--appsmith-theme-borderRadius);
box-shadow: var(--appsmith-theme-boxShadow);
padding: 29px 25px;
box-sizing: border-box;
font-family: system-ui;
background: #fff;
}
.tip-container {
margin-bottom: 20px;
font-size: 14px;
line-height: 1.571429;
}
.tip-container h2 {
margin-bottom: 20px;
font-size: 16px;
font-weight: 700;
}
.tip-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 9px;
}
.tip-header div {
color: #999;
}
.button-container {
text-align: right;
padding-top: 4px;
}
.button-container button {
margin: 0 10px;
cursor: pointer;
border-radius: var(--appsmith-theme-borderRadius);
padding: 6px 16px;
background: none;
}
.button-container button#next {
background: var(--appsmith-theme-primaryColor) !important;
color: #fff;
border:1px solid var(--appsmith-theme-primaryColor) !important;
}
.button-container button#reset {
border: 1px solid #999;
color: #999;
outline: none;
box-shadow: none;
}
.button-container button#reset:hover:not(:disabled) {
color: var(--appsmith-theme-primaryColor);
border-color: var(--appsmith-theme-primaryColor);
}
.button-container button#reset:disabled {
cursor: default;
}`,
js: `new Vue({
el: "#hello-world-app",
data() {
return {
msg: "Hello World by Vue!"
}
}
js: `appsmith.onReady(() => {
/*
* This handler function will get called when parent application is ready.
* Initialize your component here
* more info - ${CUSTOM_WIDGET_ONREADY_DOC_URL}
*/
new Vue({
el: "#app",
data() {
return {
currentIndex: 0,
tips: appsmith.model.tips,
};
},
methods: {
next() {
this.currentIndex = (this.currentIndex + 1) % this.tips.length;
},
reset() {
this.currentIndex = 0;
appsmith.triggerEvent("onReset");
},
},
});
});`,
},
};

View File

@ -21,14 +21,14 @@ export const DEFAULT_CONTEXT_VALUE = {
name: "",
widgetId: "",
srcDoc: {
html: "<div>Hello World</div>",
js: "function test() {console.log('Hello World');}",
css: "div {color: red;}",
html: "",
js: "",
css: "",
},
uncompiledSrcDoc: {
html: "<div>Hello World</div>",
js: "function test() {console.log('Hello World');}",
css: "div {color: red;}",
html: "",
js: "",
css: "",
},
model: {},
events: {},
@ -59,3 +59,6 @@ export const CUSTOM_WIDGET_DOC_URL =
export const CUSTOM_WIDGET_DEFAULT_MODEL_DOC_URL =
"https://docs.appsmith.com/reference/widgets/custom#default-model";
export const CUSTOM_WIDGET_ONREADY_DOC_URL =
"https://docs.appsmith.com/reference/widgets/custom#onready";

View File

@ -56,14 +56,14 @@ describe("compileSrcDoc", () => {
const result = compileSrcDoc(validSrcDoc);
expect(result.code).toEqual(validSrcDoc);
expect(result.warnings).toHaveLength(0);
expect(result.warnings).toHaveLength(1);
expect(result.errors).toHaveLength(0);
});
it("should handle Babel compilation errors", () => {
const srcDocWithErrors = {
html: "<div>Hello World</div>",
js: "const a = 5 )",
js: "appsmith.onReady(() => {const a = 5 )})",
css: "div { color: red; }",
};

View File

@ -1,5 +1,10 @@
import { transform } from "@babel/standalone/";
import type { DebuggerLogItem, SrcDoc } from "./types";
import {
CUSTOM_WIDGET_FEATURE,
createMessage,
} from "@appsmith/constants/messages";
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "./constants";
interface CompiledResult {
code: SrcDoc;
@ -14,6 +19,8 @@ export const compileSrcDoc = (srcDoc: SrcDoc): CompiledResult => {
errors: [],
};
checkForWarnings(compiledResult);
try {
const result = transform(srcDoc.js, {
sourceType: "module",
@ -34,6 +41,24 @@ export const compileSrcDoc = (srcDoc: SrcDoc): CompiledResult => {
return compiledResult;
};
function checkForWarnings(compiledResult: CompiledResult) {
const code = compiledResult.code.js;
if (code?.length > 0) {
/*
* We are keeping this check as a simple string check instead of using AST
* because we want to keep the custom widget compile process as simple as possible.
*/
!code.includes("appsmith.onReady(") &&
compiledResult.warnings.push({
message: createMessage(
CUSTOM_WIDGET_FEATURE.debugger.noOnReadyWarning,
CUSTOM_WIDGET_ONREADY_DOC_URL,
),
});
}
}
export interface BabelError {
reasonCode: string;
message: string;

View File

@ -125,10 +125,10 @@ function NewActionButton(props: NewActionButtonProps) {
height={pages.length <= 4 ? "fit-content" : "186px"}
side={"bottom"}
>
<Text className="pl-2" kind="heading-xs">{`Create a ${
<Text className="pl-2" kind="heading-xs">{`Create ${
pluginType === PluginType.DB || pluginType === PluginType.SAAS
? "query"
: "api"
: "API"
} in`}</Text>
{pageMenuItems.map((page, i) => {
if (page) {

View File

@ -3,7 +3,7 @@ import { Flex } from "design-system";
import { useCurrentEditorState } from "../hooks";
import { EditorEntityTab } from "@appsmith/entities/IDE/constants";
import { useSelector } from "react-redux";
import type { EntityItem } from "@appsmith/selectors/appIDESelectors";
import type { EntityItem } from "@appsmith/selectors/entitiesSelector";
import {
selectJSSegmentEditorTabs,
selectQuerySegmentEditorTabs,

View File

@ -2,7 +2,7 @@ import React from "react";
import clsx from "classnames";
import { useSelector } from "react-redux";
import type { EntityItem } from "@appsmith/selectors/appIDESelectors";
import type { EntityItem } from "@appsmith/selectors/entitiesSelector";
import { getCurrentPageId } from "@appsmith/selectors/entitiesSelector";
import { useActiveAction } from "@appsmith/pages/Editor/Explorer/hooks";
import { jsCollectionIdURL } from "@appsmith/RouteBuilder";

View File

@ -2,10 +2,12 @@ import React, { useMemo } from "react";
import clsx from "classnames";
import { useSelector } from "react-redux";
import type { EntityItem } from "@appsmith/selectors/appIDESelectors";
import { getAction } from "@appsmith/selectors/entitiesSelector";
import { getPlugins } from "@appsmith/selectors/entitiesSelector";
import { getCurrentPageId } from "@appsmith/selectors/entitiesSelector";
import type { EntityItem } from "@appsmith/selectors/entitiesSelector";
import {
getAction,
getPlugins,
getCurrentPageId,
} from "@appsmith/selectors/entitiesSelector";
import { useActiveAction } from "@appsmith/pages/Editor/Explorer/hooks";
import history, { NavigationMethod } from "utils/history";
import { getActionConfig } from "pages/Editor/Explorer/Actions/helpers";

View File

@ -64,10 +64,8 @@ import {
SlashCommand,
} from "entities/Action";
import type { ActionData } from "@appsmith/reducers/entityReducers/actionsReducer";
import type {
EditorSegmentList,
EntityItem,
} from "@appsmith/selectors/appIDESelectors";
import type { EditorSegmentList } from "@appsmith/selectors/appIDESelectors";
import type { EntityItem } from "@appsmith/selectors/entitiesSelector";
import { selectQuerySegmentEditorList } from "@appsmith/selectors/appIDESelectors";
import {
getAction,

View File

@ -1,5 +1,4 @@
import { Colors } from "constants/Colors";
import { FEATURE_FLAG } from "@appsmith/entities/FeatureFlag";
export type ChartType =
| "LINE_CHART"
@ -85,12 +84,6 @@ export const messages = {
},
};
export const CUSTOM_ECHART_FEATURE_FLAG =
FEATURE_FLAG["release_custom_echarts_enabled"];
export const FUSION_CHART_DEPRECATION_FLAG =
FEATURE_FLAG["deprecate_custom_fusioncharts_enabled"];
export const CUSTOM_CHART_TYPES = [
"area2d",
"bar2d",

View File

@ -148,10 +148,6 @@ describe("emptyChartData", () => {
});
describe("Widget Callouts", () => {
ChartWidget.showCustomFusionChartDeprecationMessages = jest
.fn()
.mockReturnValue(true);
it("returns custom fusion chart deprecation notice when chart type is custom fusion chart", () => {
const props = JSON.parse(JSON.stringify(defaultProps));
props.chartType = "CUSTOM_FUSION_CHART";

View File

@ -6,11 +6,9 @@ import { retryPromise } from "utils/AppsmithUtils";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { contentConfig, styleConfig } from "./propertyConfig";
import {
CUSTOM_ECHART_FEATURE_FLAG,
DefaultEChartConfig,
DefaultEChartsBasicChartsData,
DefaultFusionChartConfig,
FUSION_CHART_DEPRECATION_FLAG,
messages,
} from "../constants";
import type { ChartSelectedDataPoint } from "../constants";
@ -148,10 +146,7 @@ class ChartWidget extends BaseWidget<ChartWidgetProps, WidgetState> {
return {
getEditorCallouts(props: WidgetProps): WidgetCallout[] {
const callouts: WidgetCallout[] = [];
if (
ChartWidget.showCustomFusionChartDeprecationMessages() &&
props.chartType == "CUSTOM_FUSION_CHART"
) {
if (props.chartType == "CUSTOM_FUSION_CHART") {
callouts.push({
message: messages.customFusionChartDeprecationMessage,
links: [
@ -190,20 +185,13 @@ class ChartWidget extends BaseWidget<ChartWidgetProps, WidgetState> {
}
static getPropertyPaneContentConfig() {
return contentConfig(
this.getFeatureFlag(CUSTOM_ECHART_FEATURE_FLAG),
this.showCustomFusionChartDeprecationMessages(),
);
return contentConfig();
}
static getPropertyPaneStyleConfig() {
return styleConfig;
}
static showCustomFusionChartDeprecationMessages() {
return this.getFeatureFlag(FUSION_CHART_DEPRECATION_FLAG);
}
static getStylesheetConfig(): Stylesheet {
return {
borderRadius: "{{appsmith.theme.borderRadius.appBorderRadius}}",

View File

@ -4,12 +4,7 @@ import { isString } from "lodash";
import { styleConfig, contentConfig } from "./propertyConfig";
import type { PropertyPaneControlConfig } from "constants/PropertyControlConstants";
const customEChartsEnabled = true;
const showFusionChartDeprecationMessage = true;
const config = [
...contentConfig(customEChartsEnabled, showFusionChartDeprecationMessage),
...styleConfig,
];
const config = [...contentConfig(), ...styleConfig];
declare global {
namespace jest {

View File

@ -5,17 +5,13 @@ import {
CUSTOM_CHART_TYPES,
LabelOrientation,
LABEL_ORIENTATION_COMPATIBLE_CHARTS,
messages,
} from "../constants";
import type { WidgetProps } from "widgets/BaseWidget";
export const isLabelOrientationApplicableFor = (chartType: string) =>
LABEL_ORIENTATION_COMPATIBLE_CHARTS.includes(chartType);
const labelOptions = (
customEChartsEnabled: boolean,
showCustomFusionChartDeprecationMessage: boolean,
) => {
const labelOptions = () => {
const options = [
{
label: "Line chart",
@ -38,26 +34,19 @@ const labelOptions = (
value: "AREA_CHART",
},
{
label: messages.customFusionChartOptionLabel(
showCustomFusionChartDeprecationMessage,
),
label: "Custom EChart",
value: "CUSTOM_ECHART",
},
{
label: "Custom Fusion Charts (deprecated)",
value: "CUSTOM_FUSION_CHART",
},
];
if (customEChartsEnabled) {
options.splice(options.length - 1, 0, {
label: "Custom EChart",
value: "CUSTOM_ECHART",
});
}
return options;
};
export const contentConfig = (
customEChartsEnabled: boolean,
showCustomFusionChartDeprecationMessage: boolean,
) => {
export const contentConfig = () => {
return [
{
sectionName: "Data",
@ -67,10 +56,7 @@ export const contentConfig = (
propertyName: "chartType",
label: "Chart type",
controlType: "DROP_DOWN",
options: labelOptions(
customEChartsEnabled,
showCustomFusionChartDeprecationMessage,
),
options: labelOptions(),
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,

View File

@ -25,7 +25,7 @@
},
});
["log", "warn", "info"].forEach((method) => {
["log", "warn", "info", "error"].forEach((method) => {
nativeConsole[method] = createProxy(method);
});

View File

@ -20,9 +20,8 @@ import type { Color } from "constants/Colors";
import { connect } from "react-redux";
import type { AppState } from "@appsmith/reducers";
import { combinedPreviewModeSelector } from "selectors/editorSelectors";
import { getAppMode } from "@appsmith/selectors/applicationSelectors";
import { APP_MODE } from "entities/App";
import { getWidgetPropsForPropertyPane } from "selectors/propertyPaneSelectors";
import AnalyticsUtil from "utils/AnalyticsUtil";
const StyledIframe = styled.iframe<{ width: number; height: number }>`
width: ${(props) => props.width - 8}px;
@ -101,6 +100,16 @@ function CustomComponent(props: CustomComponentProps) {
},
"*",
);
if (
props.renderMode === "DEPLOYED" ||
props.renderMode === "EDITOR"
) {
AnalyticsUtil.logEvent("CUSTOM_WIDGET_LOAD_INIT", {
widgetId: props.widgetId,
renderMode: props.renderMode,
});
}
break;
case EVENTS.CUSTOM_WIDGET_UPDATE_MODEL:
props.update(message.data);
@ -256,11 +265,10 @@ export const mapStateToProps = (
ownProps: CustomComponentProps,
) => {
const isPreviewMode = combinedPreviewModeSelector(state);
const appMode = getAppMode(state);
return {
needsOverlay:
appMode == APP_MODE.EDIT &&
ownProps.renderMode === "EDITOR" &&
!isPreviewMode &&
ownProps.widgetId !== getWidgetPropsForPropertyPane(state)?.widgetId,
};

View File

@ -1,3 +1,5 @@
import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants";
export default {
uncompiledSrcDoc: {
html: `<!-- no need to write html, head, body tags, it is handled by the widget -->
@ -82,6 +84,11 @@ function App() {
}
appsmith.onReady(() => {
/*
* This handler function will get called when parent application is ready.
* Initialize your component here
* more info - ${CUSTOM_WIDGET_ONREADY_DOC_URL}
*/
reactDom.render(<App />, document.getElementById("root"));
});`,
},

View File

@ -31,6 +31,7 @@ import { Link } from "design-system";
import styled from "styled-components";
import { ReduxActionTypes } from "@appsmith/constants/ReduxActionConstants";
import { Colors } from "constants/Colors";
import AnalyticsUtil from "utils/AnalyticsUtil";
const StyledLink = styled(Link)`
display: inline-block;
@ -332,6 +333,11 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
},
globalContext: contextObj,
});
AnalyticsUtil.logEvent("CUSTOM_WIDGET_API_TRIGGER_EVENT", {
widgetId: this.props.widgetId,
eventName,
});
}
};
@ -340,6 +346,10 @@ class CustomWidget extends BaseWidget<CustomWidgetProps, WidgetState> {
...this.props.model,
...data,
});
AnalyticsUtil.logEvent("CUSTOM_WIDGET_API_UPDATE_MODEL", {
widgetId: this.props.widgetId,
});
};
getRenderMode = () => {

View File

@ -13359,7 +13359,7 @@ __metadata:
cypress-xpath: ^1.6.0
dayjs: ^1.10.6
deep-diff: ^1.0.2
design-system: "npm:@appsmithorg/design-system@2.1.30"
design-system: "npm:@appsmithorg/design-system@2.1.31"
design-system-old: "npm:@appsmithorg/design-system-old@1.1.14"
diff: ^5.0.0
dotenv: ^8.1.0
@ -17407,9 +17407,9 @@ __metadata:
languageName: node
linkType: hard
"design-system@npm:@appsmithorg/design-system@2.1.30":
version: 2.1.30
resolution: "@appsmithorg/design-system@npm:2.1.30"
"design-system@npm:@appsmithorg/design-system@2.1.31":
version: 2.1.31
resolution: "@appsmithorg/design-system@npm:2.1.31"
dependencies:
"@radix-ui/react-dialog": ^1.0.2
"@radix-ui/react-dropdown-menu": ^2.0.4
@ -17439,7 +17439,7 @@ __metadata:
react-dom: ^17.0.2
react-router-dom: ^5.0.0
styled-components: ^5.3.6
checksum: b41c9a2f85db6c7ed59c74cabfb874a27d9df9fdaa11dd2e33a687be5df653c3d8f6547e332af625f7061b168ae1375068255535d8e31642b69ea8c0ca8e415e
checksum: 4fc89bbb7f4403a9583960dd410c722ed3469e22c3e3d3bc256b3024ce6a7288f46308ef89386d931c264e9179cf22141f673f1c0ed9675dc60e8401c3845be8
languageName: node
linkType: hard

View File

@ -75,12 +75,13 @@ public class ActionCollectionImportableServiceCEImpl implements ImportableServic
MappedImportableResourcesDTO mappedImportableResourcesDTO) {
Mono<List<ActionCollection>> importedActionCollectionMono = Mono.just(importedActionCollectionList);
if (importingMetaDTO.getAppendToApp()) {
if (importingMetaDTO.getAppendToArtifact()) {
importedActionCollectionMono = importedActionCollectionMono.map(importedActionCollectionList1 -> {
List<NewPage> importedNewPages = mappedImportableResourcesDTO.getPageNameMap().values().stream()
List<NewPage> importedNewPages = mappedImportableResourcesDTO.getPageOrModuleMap().values().stream()
.distinct()
.map(branchAwareDomain -> (NewPage) branchAwareDomain)
.toList();
Map<String, String> newToOldNameMap = mappedImportableResourcesDTO.getNewPageNameToOldPageNameMap();
Map<String, String> newToOldNameMap = mappedImportableResourcesDTO.getPageOrModuleNewNameToOldName();
for (NewPage newPage : importedNewPages) {
String newPageName = newPage.getUnpublishedPage().getName();
@ -194,7 +195,8 @@ public class ActionCollectionImportableServiceCEImpl implements ImportableServic
.getPluginMap()
.get(unpublishedCollection.getPluginId()));
parentPage = updatePageInActionCollection(
unpublishedCollection, mappedImportableResourcesDTO.getPageNameMap());
unpublishedCollection, (Map<String, NewPage>)
mappedImportableResourcesDTO.getPageOrModuleMap());
}
if (publishedCollection != null && publishedCollection.getName() != null) {
@ -209,8 +211,9 @@ public class ActionCollectionImportableServiceCEImpl implements ImportableServic
if (StringUtils.isEmpty(publishedCollection.getPageId())) {
publishedCollection.setPageId(fallbackParentPageId);
}
NewPage publishedCollectionPage = updatePageInActionCollection(
publishedCollection, mappedImportableResourcesDTO.getPageNameMap());
NewPage publishedCollectionPage =
updatePageInActionCollection(publishedCollection, (Map<String, NewPage>)
mappedImportableResourcesDTO.getPageOrModuleMap());
parentPage = parentPage == null ? publishedCollectionPage : parentPage;
}

View File

@ -0,0 +1,711 @@
package com.appsmith.server.applications.base;
import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.external.models.Datasource;
import com.appsmith.external.models.DatasourceStorageDTO;
import com.appsmith.server.applications.imports.ApplicationImportServiceCE;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.datasources.base.DatasourceService;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ApplicationPage;
import com.appsmith.server.domains.CustomJSLib;
import com.appsmith.server.domains.ImportableArtifact;
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.domains.Workspace;
import com.appsmith.server.dtos.ApplicationImportDTO;
import com.appsmith.server.dtos.ApplicationJson;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import com.appsmith.server.imports.importable.ImportableService;
import com.appsmith.server.migrations.ApplicationVersion;
import com.appsmith.server.newactions.base.NewActionService;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.WorkspaceService;
import com.appsmith.server.solutions.ActionPermission;
import com.appsmith.server.solutions.ApplicationPermission;
import com.appsmith.server.solutions.DatasourcePermission;
import com.appsmith.server.solutions.PagePermission;
import com.appsmith.server.solutions.WorkspacePermission;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.dao.DuplicateKeyException;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import static com.appsmith.server.helpers.ImportExportUtils.setPropertiesToExistingApplication;
import static com.appsmith.server.helpers.ImportExportUtils.setPublishedApplicationProperties;
/**
* This service is currently not in use, however this service will replace ImportApplicationService
*/
@Slf4j
public class ApplicationImportServiceCEImpl implements ApplicationImportServiceCE {
private final DatasourceService datasourceService;
private final WorkspaceService workspaceService;
private final ApplicationService applicationService;
private final ApplicationPageService applicationPageService;
private final NewActionService newActionService;
private final AnalyticsService analyticsService;
private final DatasourcePermission datasourcePermission;
private final WorkspacePermission workspacePermission;
private final ApplicationPermission applicationPermission;
private final PagePermission pagePermission;
private final ActionPermission actionPermission;
private final Gson gson;
private final ImportableService<Theme> themeImportableService;
private final ImportableService<NewPage> newPageImportableService;
private final ImportableService<CustomJSLib> customJSLibImportableService;
private final ImportableService<NewAction> newActionImportableService;
private final ImportableService<ActionCollection> actionCollectionImportableService;
/**
* This map keeps constants which are specific to context of Application, parallel to other Artifacts.
* i.e. Artifact --> Application
* i.e. ID --> applicationId
*/
protected final Map<String, String> applicationConstantsMap = new HashMap<>();
public ApplicationImportServiceCEImpl(
DatasourceService datasourceService,
WorkspaceService workspaceService,
ApplicationService applicationService,
ApplicationPageService applicationPageService,
NewActionService newActionService,
AnalyticsService analyticsService,
DatasourcePermission datasourcePermission,
WorkspacePermission workspacePermission,
ApplicationPermission applicationPermission,
PagePermission pagePermission,
ActionPermission actionPermission,
Gson gson,
ImportableService<Theme> themeImportableService,
ImportableService<NewPage> newPageImportableService,
ImportableService<CustomJSLib> customJSLibImportableService,
ImportableService<NewAction> newActionImportableService,
ImportableService<ActionCollection> actionCollectionImportableService) {
this.datasourceService = datasourceService;
this.workspaceService = workspaceService;
this.applicationService = applicationService;
this.applicationPageService = applicationPageService;
this.newActionService = newActionService;
this.analyticsService = analyticsService;
this.datasourcePermission = datasourcePermission;
this.workspacePermission = workspacePermission;
this.applicationPermission = applicationPermission;
this.pagePermission = pagePermission;
this.actionPermission = actionPermission;
this.gson = gson;
this.themeImportableService = themeImportableService;
this.newPageImportableService = newPageImportableService;
this.customJSLibImportableService = customJSLibImportableService;
this.newActionImportableService = newActionImportableService;
this.actionCollectionImportableService = actionCollectionImportableService;
applicationConstantsMap.putAll(
Map.of(FieldName.ARTIFACT_CONTEXT, FieldName.APPLICATION, FieldName.ID, FieldName.APPLICATION_ID));
}
@Override
public ApplicationJson extractArtifactExchangeJson(String jsonString) {
Type fileType = new TypeToken<ApplicationJson>() {}.getType();
return gson.fromJson(jsonString, fileType);
}
@Override
public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForImportingArtifact(
Set<String> userPermissionGroups) {
return ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
datasourcePermission,
workspacePermission)
.requiredPermissionOnTargetWorkspace(workspacePermission.getApplicationCreatePermission())
.permissionRequiredToCreateDatasource(true)
.permissionRequiredToEditDatasource(true)
.currentUserPermissionGroups(userPermissionGroups)
.build();
}
@Override
public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForUpdatingArtifact(
Set<String> userPermissions) {
return ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
datasourcePermission,
workspacePermission)
.requiredPermissionOnTargetWorkspace(workspacePermission.getReadPermission())
.requiredPermissionOnTargetApplication(applicationPermission.getEditPermission())
.allPermissionsRequired()
.currentUserPermissionGroups(userPermissions)
.build();
}
/**
* If the application is connected to git, then the user must have edit permission on the application.
* If user is importing application from Git, create application permission is already checked by the
* caller method, so it's not required here.
* Other permissions are not required because Git is the source of truth for the application and Git
* Sync is a system level operation to get the latest code from Git. If the user does not have some
* permissions on the Application e.g. create page, that'll be checked when the user tries to create a page.
*/
@Override
public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForConnectingToGit(
Set<String> userPermissions) {
return ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
datasourcePermission,
workspacePermission)
.requiredPermissionOnTargetApplication(applicationPermission.getEditPermission())
.currentUserPermissionGroups(userPermissions)
.build();
}
@Override
public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForRestoringSnapshot(
Set<String> userPermissions) {
return ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
datasourcePermission,
workspacePermission)
.requiredPermissionOnTargetWorkspace(workspacePermission.getReadPermission())
.requiredPermissionOnTargetApplication(applicationPermission.getEditPermission())
.currentUserPermissionGroups(userPermissions)
.build();
}
@Override
public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForMergingJsonWithArtifact(
Set<String> userPermissions) {
return ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
datasourcePermission,
workspacePermission)
.requiredPermissionOnTargetWorkspace(workspacePermission.getReadPermission())
.requiredPermissionOnTargetApplication(applicationPermission.getEditPermission())
.allPermissionsRequired()
.currentUserPermissionGroups(userPermissions)
.build();
}
/**
* this method removes the application name from Json file as updating the app-name is not supported via import
* this avoids name conflict during import flow within workspace
*
* @param applicationId : ID of the application which has been saved.
* @param artifactExchangeJson : the ArtifactExchangeJSON which is getting imported
*/
@Override
public void setJsonArtifactNameToNullBeforeUpdate(String applicationId, ArtifactExchangeJson artifactExchangeJson) {
ApplicationJson applicationJson = (ApplicationJson) artifactExchangeJson;
if (!StringUtils.isEmpty(applicationId) && (applicationJson).getExportedApplication() != null) {
applicationJson.getExportedApplication().setName(null);
applicationJson.getExportedApplication().setSlug(null);
}
}
protected List<Mono<Void>> getPageDependentImportables(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<Application> importedApplicationMono,
ApplicationJson applicationJson) {
// Requires pageNameMap, pageNameToOldNameMap, pluginMap and datasourceNameToIdMap to be present in importable
// resources.
// Updates actionResultDTO in importable resources.
// Also, directly updates required information in DB
Mono<Void> importedNewActionsMono = newActionImportableService.importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedApplicationMono,
applicationJson,
false);
// Requires pageNameMap, pageNameToOldNameMap, pluginMap and actionResultDTO to be present in importable
// resources.
// Updates actionCollectionResultDTO in importable resources.
// Also, directly updates required information in DB
Mono<Void> importedActionCollectionsMono = actionCollectionImportableService.importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedApplicationMono,
applicationJson,
false);
Mono<Void> combinedActionImportablesMono = importedNewActionsMono.then(importedActionCollectionsMono);
return List.of(combinedActionImportablesMono);
}
@Override
public Mono<ApplicationImportDTO> getImportableArtifactDTO(
String workspaceId, String applicationId, ImportableArtifact importableArtifact) {
Application application = (Application) importableArtifact;
return findDatasourceByApplicationId(applicationId, workspaceId)
.zipWith(workspaceService.getDefaultEnvironmentId(workspaceId, null))
.map(tuple2 -> {
List<Datasource> datasources = tuple2.getT1();
String environmentId = tuple2.getT2();
ApplicationImportDTO applicationImportDTO = new ApplicationImportDTO();
applicationImportDTO.setApplication(application);
Boolean isUnConfiguredDatasource = datasources.stream().anyMatch(datasource -> {
DatasourceStorageDTO datasourceStorageDTO =
datasource.getDatasourceStorages().get(environmentId);
if (datasourceStorageDTO == null) {
// If this environment has not been configured,
// We do not expect to find a storage, user will have to reconfigure
return Boolean.FALSE;
}
return Boolean.FALSE.equals(datasourceStorageDTO.getIsConfigured());
});
if (Boolean.TRUE.equals(isUnConfiguredDatasource)) {
applicationImportDTO.setIsPartialImport(true);
applicationImportDTO.setUnConfiguredDatasourceList(datasources);
} else {
applicationImportDTO.setIsPartialImport(false);
}
return applicationImportDTO;
});
}
@Override
public Mono<List<Datasource>> findDatasourceByApplicationId(String applicationId, String workspaceId) {
// TODO: Investigate further why datasourcePermission.getReadPermission() is not being used.
Mono<List<Datasource>> listMono = datasourceService
.getAllByWorkspaceIdWithStorages(workspaceId, Optional.empty())
.collectList();
return newActionService
.findAllByApplicationIdAndViewMode(applicationId, false, Optional.empty(), Optional.empty())
.collectList()
.zipWith(listMono)
.flatMap(objects -> {
List<Datasource> datasourceList = objects.getT2();
List<NewAction> actionList = objects.getT1();
List<String> usedDatasource = actionList.stream()
.map(newAction -> newAction
.getUnpublishedAction()
.getDatasource()
.getId())
.toList();
datasourceList.removeIf(datasource -> !usedDatasource.contains(datasource.getId()));
return Mono.just(datasourceList);
});
}
@Override
public void updateArtifactExchangeJsonWithEntitiesToBeConsumed(
ArtifactExchangeJson artifactExchangeJson, List<String> entitiesToImport) {
ApplicationJson applicationJson = (ApplicationJson) artifactExchangeJson;
// Update the application JSON to prepare it for merging inside an existing application
if (applicationJson.getExportedApplication() != null) {
// setting some properties to null so that target application is not updated by these properties
applicationJson.getExportedApplication().setName(null);
applicationJson.getExportedApplication().setSlug(null);
applicationJson.getExportedApplication().setForkingEnabled(null);
applicationJson.getExportedApplication().setForkWithConfiguration(null);
applicationJson.getExportedApplication().setClonedFromApplicationId(null);
applicationJson.getExportedApplication().setExportWithConfiguration(null);
}
// need to remove git sync id. Also filter pages if pageToImport is not empty
if (applicationJson.getPageList() != null) {
List<ApplicationPage> applicationPageList =
new ArrayList<>(applicationJson.getPageList().size());
List<String> pageNames =
new ArrayList<>(applicationJson.getPageList().size());
List<NewPage> importedNewPageList = applicationJson.getPageList().stream()
.filter(newPage -> newPage.getUnpublishedPage() != null
&& (CollectionUtils.isEmpty(entitiesToImport)
|| entitiesToImport.contains(
newPage.getUnpublishedPage().getName())))
.peek(newPage -> {
ApplicationPage applicationPage = new ApplicationPage();
applicationPage.setId(newPage.getUnpublishedPage().getName());
applicationPage.setIsDefault(false);
applicationPageList.add(applicationPage);
pageNames.add(applicationPage.getId());
})
.peek(newPage -> newPage.setGitSyncId(null))
.collect(Collectors.toList());
applicationJson.setPageList(importedNewPageList);
// Remove the pages from the exported Application inside the json based on the pagesToImport
applicationJson.getExportedApplication().setPages(applicationPageList);
applicationJson.getExportedApplication().setPublishedPages(applicationPageList);
}
if (applicationJson.getActionList() != null) {
List<NewAction> importedNewActionList = applicationJson.getActionList().stream()
.filter(newAction -> newAction.getUnpublishedAction() != null
&& (CollectionUtils.isEmpty(entitiesToImport)
|| entitiesToImport.contains(
newAction.getUnpublishedAction().getPageId())))
.peek(newAction ->
newAction.setGitSyncId(null)) // setting this null so that this action can be imported again
.collect(Collectors.toList());
applicationJson.setActionList(importedNewActionList);
}
if (applicationJson.getActionCollectionList() != null) {
List<ActionCollection> importedActionCollectionList = applicationJson.getActionCollectionList().stream()
.filter(actionCollection -> (CollectionUtils.isEmpty(entitiesToImport)
|| entitiesToImport.contains(
actionCollection.getUnpublishedCollection().getPageId())))
.peek(actionCollection -> actionCollection.setGitSyncId(
null)) // setting this null so that this action collection can be imported again
.collect(Collectors.toList());
applicationJson.setActionCollectionList(importedActionCollectionList);
}
}
/**
* To send analytics event for import and export of application
*
* @param application Application object imported or exported
* @param event AnalyticsEvents event
* @return The application which is imported or exported
*/
private Mono<Application> sendImportExportApplicationAnalyticsEvent(
Application application, AnalyticsEvents event) {
return workspaceService.getById(application.getWorkspaceId()).flatMap(workspace -> {
final Map<String, Object> eventData = Map.of(
FieldName.APPLICATION, application,
FieldName.WORKSPACE, workspace);
final Map<String, Object> data = Map.of(
FieldName.APPLICATION_ID, application.getId(),
FieldName.WORKSPACE_ID, workspace.getId(),
FieldName.EVENT_DATA, eventData);
return analyticsService.sendObjectEvent(event, application, data);
});
}
/**
* To send analytics event for import and export of application
*
* @param applicationId ID of application being imported or exported
* @param event AnalyticsEvents event
* @return The application which is imported or exported
*/
private Mono<Application> sendImportExportApplicationAnalyticsEvent(String applicationId, AnalyticsEvents event) {
return applicationService
.findById(applicationId, Optional.empty())
.flatMap(application -> sendImportExportApplicationAnalyticsEvent(application, event));
}
@Override
public void syncClientAndSchemaVersion(ArtifactExchangeJson artifactExchangeJson) {
ApplicationJson applicationJson = (ApplicationJson) artifactExchangeJson;
Application importedApplication = applicationJson.getExportedApplication();
importedApplication.setServerSchemaVersion(applicationJson.getServerSchemaVersion());
importedApplication.setClientSchemaVersion(applicationJson.getClientSchemaVersion());
}
@Override
public Mono<Void> generateArtifactSpecificImportableEntities(
ArtifactExchangeJson artifactExchangeJson,
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO) {
// Persists relevant information and updates mapped resources
return customJSLibImportableService.importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
null,
null,
(ApplicationJson) artifactExchangeJson,
false);
}
@Override
public Mono<Boolean> isArtifactConnectedToGit(String artifactId) {
return applicationService.isApplicationConnectedToGit(artifactId);
}
@Override
public Mono<Application> updateAndSaveArtifactInContext(
ImportableArtifact importableArtifact,
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<User> currentUserMono) {
Mono<Application> importApplicationMono = Mono.just((Application) importableArtifact)
.map(application -> {
if (application.getApplicationVersion() == null) {
application.setApplicationVersion(ApplicationVersion.EARLIEST_VERSION);
}
application.setViewMode(false);
application.setForkWithConfiguration(null);
application.setExportWithConfiguration(null);
application.setWorkspaceId(importingMetaDTO.getWorkspaceId());
application.setIsPublic(null);
application.setPolicies(null);
Map<String, List<ApplicationPage>> mapOfApplicationPageList = Map.of(
FieldName.PUBLISHED,
application.getPublishedPages(),
FieldName.UNPUBLISHED,
application.getPages());
mappedImportableResourcesDTO
.getResourceStoreFromArtifactExchangeJson()
.putAll(mapOfApplicationPageList);
application.setPages(null);
application.setPublishedPages(null);
return application;
})
.map(application -> {
application.setUnpublishedCustomJSLibs(
new HashSet<>(mappedImportableResourcesDTO.getInstalledJsLibsList()));
return application;
});
importApplicationMono = importApplicationMono.zipWith(currentUserMono).map(objects -> {
Application application = objects.getT1();
application.setModifiedBy(objects.getT2().getUsername());
return application;
});
if (StringUtils.isEmpty(importingMetaDTO.getArtifactId())) {
importApplicationMono = importApplicationMono.flatMap(application -> {
return applicationPageService.createOrUpdateSuffixedApplication(application, application.getName(), 0);
});
} else {
Mono<Application> existingApplicationMono = applicationService
.findById(
importingMetaDTO.getArtifactId(),
importingMetaDTO.getPermissionProvider().getRequiredPermissionOnTargetApplication())
.switchIfEmpty(Mono.defer(() -> {
log.error(
"No application found with id: {} and permission: {}",
importingMetaDTO.getArtifactId(),
importingMetaDTO.getPermissionProvider().getRequiredPermissionOnTargetApplication());
return Mono.error(new AppsmithException(
AppsmithError.ACL_NO_RESOURCE_FOUND,
FieldName.APPLICATION,
importingMetaDTO.getArtifactId()));
}))
.cache();
// this can be a git sync, import page from template, update app with json, restore snapshot
if (importingMetaDTO.getAppendToArtifact()) { // we don't need to do anything with the imported application
importApplicationMono = existingApplicationMono;
} else {
importApplicationMono = importApplicationMono
.zipWith(existingApplicationMono)
.map(objects -> {
Application newApplication = objects.getT1();
Application existingApplication = objects.getT2();
// This method sets the published mode properties in the imported
// application.When a user imports an application from the git repo,
// since the git only stores the unpublished version, the current
// deployed version in the newly imported app is not updated.
// This function sets the initial deployed version to the same as the
// edit mode one.
setPublishedApplicationProperties(newApplication);
setPropertiesToExistingApplication(newApplication, existingApplication);
return existingApplication;
})
.flatMap(application -> {
Mono<Application> parentApplicationMono;
if (application.getGitApplicationMetadata() != null) {
parentApplicationMono = applicationService.findById(
application.getGitApplicationMetadata().getDefaultApplicationId());
} else {
parentApplicationMono = Mono.just(application);
}
return Mono.zip(Mono.just(application), parentApplicationMono);
})
.flatMap(objects -> {
Application application = objects.getT1();
Application parentApplication = objects.getT2();
application.setPolicies(parentApplication.getPolicies());
return applicationService
.save(application)
.onErrorResume(DuplicateKeyException.class, error -> {
if (error.getMessage() != null) {
return applicationPageService.createOrUpdateSuffixedApplication(
application, application.getName(), 0);
}
throw error;
});
});
}
}
return importApplicationMono
.elapsed()
.map(tuples -> {
log.debug("time to create or update application object: {}", tuples.getT1());
return tuples.getT2();
})
.onErrorResume(error -> {
log.error("Error while creating or updating application object", error);
return Mono.error(error);
});
}
@Override
public Mono<Application> updateImportableArtifact(ImportableArtifact importableArtifact) {
return Mono.just((Application) importableArtifact).flatMap(application -> {
log.info("Imported application with id {}", application.getId());
// Need to update the application object with updated pages and publishedPages
Application updateApplication = new Application();
updateApplication.setPages(application.getPages());
updateApplication.setPublishedPages(application.getPublishedPages());
return applicationService.update(application.getId(), updateApplication);
});
}
@Override
public Mono<Application> updateImportableEntities(
ImportableArtifact importableContext,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
ImportingMetaDTO importingMetaDTO) {
return Mono.just((Application) importableContext).flatMap(application -> {
return newActionImportableService
.updateImportedEntities(application, importingMetaDTO, mappedImportableResourcesDTO, false)
.then(newPageImportableService.updateImportedEntities(
application, importingMetaDTO, mappedImportableResourcesDTO, false))
.thenReturn(application);
});
}
@Override
public Map<String, Object> createImportAnalyticsData(
ArtifactExchangeJson artifactExchangeJson, ImportableArtifact importableArtifact) {
Application application = (Application) importableArtifact;
ApplicationJson applicationJson = (ApplicationJson) artifactExchangeJson;
int jsObjectCount = CollectionUtils.isEmpty(applicationJson.getActionCollectionList())
? 0
: applicationJson.getActionCollectionList().size();
int actionCount = CollectionUtils.isEmpty(applicationJson.getActionList())
? 0
: applicationJson.getActionList().size();
final Map<String, Object> data = Map.of(
FieldName.APPLICATION_ID,
application.getId(),
FieldName.WORKSPACE_ID,
application.getWorkspaceId(),
"pageCount",
applicationJson.getPageList().size(),
"actionCount",
actionCount,
"JSObjectCount",
jsObjectCount);
return data;
}
@Override
public Flux<Void> generateArtifactContextIndependentImportableEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importableArtifactMono,
ArtifactExchangeJson artifactExchangeJson) {
return importableArtifactMono.flatMapMany(importableContext -> {
Application application = (Application) importableContext;
ApplicationJson applicationJson = (ApplicationJson) artifactExchangeJson;
// Updates pageNametoIdMap and pageNameMap in importable resources.
// Also, directly updates required information in DB
Mono<Void> importedPagesMono = newPageImportableService.importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
Mono.just(application),
applicationJson,
false);
// Directly updates required theme information in DB
Mono<Void> importedThemesMono = themeImportableService.importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
Mono.just(application),
applicationJson,
false,
true);
return Flux.merge(List.of(importedPagesMono, importedThemesMono));
});
}
@Override
public Flux<Void> generateArtifactContextDependentImportableEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importableArtifactMono,
ArtifactExchangeJson artifactExchangeJson) {
return importableArtifactMono.flatMapMany(importableContext -> {
Application application = (Application) importableContext;
ApplicationJson applicationJson = (ApplicationJson) artifactExchangeJson;
List<Mono<Void>> pageDependentImportables = getPageDependentImportables(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
Mono.just(application),
applicationJson);
return Flux.merge(pageDependentImportables);
});
}
@Override
public String validateArtifactSpecificFields(ArtifactExchangeJson artifactExchangeJson) {
ApplicationJson importedDoc = (ApplicationJson) artifactExchangeJson;
String errorField = "";
if (CollectionUtils.isEmpty(importedDoc.getPageList())) {
errorField = FieldName.PAGE_LIST;
} else if (importedDoc.getActionList() == null) {
errorField = FieldName.ACTIONS;
} else if (importedDoc.getDatasourceList() == null) {
errorField = FieldName.DATASOURCE;
}
return errorField;
}
@Override
public Map<String, String> getArtifactSpecificConstantsMap() {
return applicationConstantsMap;
}
}

View File

@ -0,0 +1,63 @@
package com.appsmith.server.applications.base;
import com.appsmith.server.applications.imports.ApplicationImportService;
import com.appsmith.server.datasources.base.DatasourceService;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.CustomJSLib;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.imports.importable.ImportableService;
import com.appsmith.server.newactions.base.NewActionService;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.ApplicationPageService;
import com.appsmith.server.services.WorkspaceService;
import com.appsmith.server.solutions.ActionPermission;
import com.appsmith.server.solutions.ApplicationPermission;
import com.appsmith.server.solutions.DatasourcePermission;
import com.appsmith.server.solutions.PagePermission;
import com.appsmith.server.solutions.WorkspacePermission;
import com.google.gson.Gson;
import org.springframework.stereotype.Component;
@Component
public class ApplicationImportServiceImpl extends ApplicationImportServiceCEImpl implements ApplicationImportService {
public ApplicationImportServiceImpl(
DatasourceService datasourceService,
WorkspaceService workspaceService,
ApplicationService applicationService,
ApplicationPageService applicationPageService,
NewActionService newActionService,
AnalyticsService analyticsService,
DatasourcePermission datasourcePermission,
WorkspacePermission workspacePermission,
ApplicationPermission applicationPermission,
PagePermission pagePermission,
ActionPermission actionPermission,
Gson gson,
ImportableService<Theme> themeImportableService,
ImportableService<NewPage> newPageImportableService,
ImportableService<CustomJSLib> customJSLibImportableService,
ImportableService<NewAction> newActionImportableService,
ImportableService<ActionCollection> actionCollectionImportableService) {
super(
datasourceService,
workspaceService,
applicationService,
applicationPageService,
newActionService,
analyticsService,
datasourcePermission,
workspacePermission,
applicationPermission,
pagePermission,
actionPermission,
gson,
themeImportableService,
newPageImportableService,
customJSLibImportableService,
newActionImportableService,
actionCollectionImportableService);
}
}

View File

@ -0,0 +1,3 @@
package com.appsmith.server.applications.imports;
public interface ApplicationImportService extends ApplicationImportServiceCE {}

View File

@ -0,0 +1,16 @@
package com.appsmith.server.applications.imports;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.domains.Application;
import com.appsmith.server.dtos.ApplicationImportDTO;
import com.appsmith.server.dtos.ApplicationJson;
import com.appsmith.server.imports.internal.ContextBasedImportService;
import reactor.core.publisher.Mono;
import java.util.List;
public interface ApplicationImportServiceCE
extends ContextBasedImportService<Application, ApplicationImportDTO, ApplicationJson> {
Mono<List<Datasource>> findDatasourceByApplicationId(String applicationId, String orgId);
}

View File

@ -0,0 +1,11 @@
package com.appsmith.server.constants;
/**
* The type of Json which the system deals with, it could be application, packages, or workflows.
* Collectively called Artifact
*/
public enum ArtifactJsonType {
APPLICATION,
PACKAGE,
WORKFLOW
}

View File

@ -196,4 +196,7 @@ public class FieldNameCE {
public static final String INSTANCE_ID = "instanceId";
public static final String IP_ADDRESS = "ipAddress";
public static final String VERSION = "version";
public static final String PUBLISHED = "published";
public static final String UNPUBLISHED = "unpublished";
public static final String ARTIFACT_CONTEXT = "artifactContext";
}

View File

@ -6,6 +6,7 @@ import com.appsmith.server.controllers.ce.ApplicationControllerCE;
import com.appsmith.server.exports.internal.ExportApplicationService;
import com.appsmith.server.exports.internal.PartialExportService;
import com.appsmith.server.fork.internal.ApplicationForkingService;
import com.appsmith.server.imports.importable.ImportService;
import com.appsmith.server.imports.internal.ImportApplicationService;
import com.appsmith.server.imports.internal.PartialImportService;
import com.appsmith.server.services.ApplicationPageService;
@ -29,7 +30,8 @@ public class ApplicationController extends ApplicationControllerCE {
ThemeService themeService,
ApplicationSnapshotService applicationSnapshotService,
PartialExportService partialExportService,
PartialImportService partialImportService) {
PartialImportService partialImportService,
ImportService importService) {
super(
service,
applicationPageService,
@ -40,6 +42,7 @@ public class ApplicationController extends ApplicationControllerCE {
themeService,
applicationSnapshotService,
partialExportService,
partialImportService);
partialImportService,
importService);
}
}

View File

@ -14,6 +14,7 @@ import com.appsmith.server.dtos.ApplicationImportDTO;
import com.appsmith.server.dtos.ApplicationJson;
import com.appsmith.server.dtos.ApplicationPagesDTO;
import com.appsmith.server.dtos.GitAuthDTO;
import com.appsmith.server.dtos.ImportableArtifactDTO;
import com.appsmith.server.dtos.PartialExportFileDTO;
import com.appsmith.server.dtos.ReleaseItemsDTO;
import com.appsmith.server.dtos.ResponseDTO;
@ -23,6 +24,7 @@ import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.exports.internal.ExportApplicationService;
import com.appsmith.server.exports.internal.PartialExportService;
import com.appsmith.server.fork.internal.ApplicationForkingService;
import com.appsmith.server.imports.importable.ImportService;
import com.appsmith.server.imports.internal.ImportApplicationService;
import com.appsmith.server.imports.internal.PartialImportService;
import com.appsmith.server.services.ApplicationPageService;
@ -69,6 +71,7 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
private final ApplicationSnapshotService applicationSnapshotService;
private final PartialExportService partialExportService;
private final PartialImportService partialImportService;
private final ImportService importService;
@Autowired
public ApplicationControllerCE(
@ -81,7 +84,8 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
ThemeService themeService,
ApplicationSnapshotService applicationSnapshotService,
PartialExportService partialExportService,
PartialImportService partialImportService) {
PartialImportService partialImportService,
ImportService importService) {
super(service);
this.applicationPageService = applicationPageService;
this.applicationFetcher = applicationFetcher;
@ -92,6 +96,7 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
this.applicationSnapshotService = applicationSnapshotService;
this.partialExportService = partialExportService;
this.partialImportService = partialImportService;
this.importService = importService;
}
@JsonView(Views.Public.class)
@ -296,7 +301,7 @@ public class ApplicationControllerCE extends BaseController<ApplicationService,
@JsonView(Views.Public.class)
@PostMapping(value = "/import/{workspaceId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public Mono<ResponseDTO<ApplicationImportDTO>> importApplicationFromFile(
public Mono<ResponseDTO<ImportableArtifactDTO>> importApplicationFromFile(
@RequestPart("file") Mono<Part> fileMono,
@PathVariable String workspaceId,
@RequestParam(name = FieldName.APPLICATION_ID, required = false) String applicationId) {

View File

@ -13,13 +13,15 @@ import com.appsmith.external.models.OAuth2;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.datasources.base.DatasourceService;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.ApplicationJson;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.ce.ImportApplicationPermissionProvider;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import com.appsmith.server.imports.importable.ImportableServiceCE;
import com.appsmith.server.services.SequenceService;
import com.appsmith.server.services.WorkspaceService;
@ -54,6 +56,28 @@ public class DatasourceImportableServiceCEImpl implements ImportableServiceCE<Da
this.sequenceService = sequenceService;
}
@Override
public Mono<Void> importEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importContextMono,
ArtifactExchangeJson importableContextJson,
boolean isPartialImport,
boolean isContextAgnostic) {
return importContextMono.flatMap(importableContext -> {
Application application = (Application) importableContext;
ApplicationJson applicationJson = (ApplicationJson) importableContextJson;
return importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
Mono.just(application),
applicationJson,
isPartialImport);
});
}
// Requires pluginMap to be present in importable resources.
// Updates datasourceNameToIdMap in importable resources.
// Also directly updates required information in DB
@ -71,7 +95,7 @@ public class DatasourceImportableServiceCEImpl implements ImportableServiceCE<Da
.cache();
Mono<List<Datasource>> existingDatasourceMono =
getExistingDatasourceMono(importingMetaDTO.getApplicationId(), existingDatasourceFlux);
getExistingDatasourceMono(importingMetaDTO.getArtifactId(), existingDatasourceFlux);
Mono<Map<String, String>> datasourceMapMono = importDatasources(
applicationJson,
existingDatasourceMono,
@ -247,7 +271,7 @@ public class DatasourceImportableServiceCEImpl implements ImportableServiceCE<Da
DatasourceStorage datasourceStorage,
Workspace workspace,
String environmentId,
ImportApplicationPermissionProvider permissionProvider) {
ImportArtifactPermissionProvider permissionProvider) {
/*
1. If same datasource is present return
2. If unable to find the datasource create a new datasource with unique name and return

View File

@ -34,7 +34,7 @@ import static com.appsmith.server.helpers.DateUtils.ISO_FORMATTER;
@NoArgsConstructor
@QueryEntity
@Document
public class Application extends BaseDomain {
public class Application extends BaseDomain implements ImportableArtifact {
@NotNull @JsonView(Views.Public.class)
String name;

View File

@ -0,0 +1,5 @@
package com.appsmith.server.domains;
import com.appsmith.server.domains.ce.ImportableArtifactCE;
public interface ImportableArtifact extends ImportableArtifactCE {}

View File

@ -0,0 +1,8 @@
package com.appsmith.server.domains.ce;
public interface ImportableArtifactCE {
String getId();
String getWorkspaceId();
}

View File

@ -9,7 +9,8 @@ import java.util.List;
@Getter
@Setter
public class ApplicationImportDTO {
public class ApplicationImportDTO extends ImportableArtifactDTO {
Application application;
List<Datasource> unConfiguredDatasourceList;

View File

@ -0,0 +1,5 @@
package com.appsmith.server.dtos;
import com.appsmith.server.dtos.ce.ArtifactExchangeJsonCE;
public interface ArtifactExchangeJson extends ArtifactExchangeJsonCE {}

View File

@ -0,0 +1,3 @@
package com.appsmith.server.dtos;
public abstract class ImportableArtifactDTO {}

View File

@ -1,6 +1,6 @@
package com.appsmith.server.dtos;
import com.appsmith.server.helpers.ce.ImportApplicationPermissionProvider;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
@ -14,9 +14,19 @@ import java.util.Set;
@Builder(toBuilder = true)
public class ImportingMetaDTO {
String workspaceId;
String applicationId;
/**
* this represents any parent entity's id which could be imported.
* e.g. application, packages, workflows
*/
String artifactId;
String branchName;
Boolean appendToApp;
ImportApplicationPermissionProvider permissionProvider;
/**
* this flag is for verifying whether the artifact in focus needs to be updated with the given provided json
*/
Boolean appendToArtifact;
ImportArtifactPermissionProvider permissionProvider;
Set<String> currentUserPermissionGroups;
}

View File

@ -5,16 +5,18 @@ import com.appsmith.external.models.DatasourceStorageStructure;
import com.appsmith.external.models.DecryptedSensitiveFields;
import com.appsmith.external.models.InvisibleActionFields;
import com.appsmith.external.views.Views;
import com.appsmith.server.constants.ArtifactJsonType;
import com.appsmith.server.domains.ActionCollection;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.CustomJSLib;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.domains.NewAction;
import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.fasterxml.jackson.annotation.JsonView;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Transient;
import java.util.List;
import java.util.Map;
@ -27,17 +29,15 @@ import java.util.Set;
*/
@Getter
@Setter
public class ApplicationJsonCE {
public class ApplicationJsonCE implements ArtifactExchangeJson {
// To convey the schema version of the client and will be used to check if the imported file is compatible with
// current DSL schema
@Transient
@JsonView({Views.Public.class, Views.Export.class})
Integer clientSchemaVersion;
// To convey the schema version of the server and will be used to check if the imported file is compatible with
// current DB schema
@Transient
@JsonView({Views.Public.class, Views.Export.class})
Integer serverSchemaVersion;
@ -114,4 +114,19 @@ public class ApplicationJsonCE {
@JsonView({Views.Public.class, Views.Export.class})
String widgets;
@Override
public ArtifactJsonType getArtifactJsonType() {
return ArtifactJsonType.APPLICATION;
}
@Override
public ImportableArtifact getImportableArtifact() {
return this.getExportedApplication();
}
@Override
public List<CustomJSLib> getCustomJsLibFromArtifact() {
return this.getCustomJSLibList();
}
}

View File

@ -0,0 +1,24 @@
package com.appsmith.server.dtos.ce;
import com.appsmith.server.constants.ArtifactJsonType;
import com.appsmith.server.domains.CustomJSLib;
import com.appsmith.server.domains.ImportableArtifact;
import java.util.List;
public interface ArtifactExchangeJsonCE {
Integer getClientSchemaVersion();
void setClientSchemaVersion(Integer clientSchemaVersion);
Integer getServerSchemaVersion();
void setServerSchemaVersion(Integer serverSchemaVersion);
ArtifactJsonType getArtifactJsonType();
ImportableArtifact getImportableArtifact();
List<CustomJSLib> getCustomJsLibFromArtifact();
}

View File

@ -1,6 +1,6 @@
package com.appsmith.server.dtos.ce;
import com.appsmith.server.domains.NewPage;
import com.appsmith.external.models.BranchAwareDomain;
import com.appsmith.server.dtos.CustomJSLibContextDTO;
import com.appsmith.server.dtos.ImportActionCollectionResultDTO;
import com.appsmith.server.dtos.ImportActionResultDTO;
@ -16,13 +16,27 @@ import java.util.Map;
@Data
public class MappedImportableResourcesCE_DTO {
// Artifacts independent entities
Map<String, String> pluginMap = new HashMap<>();
Map<String, String> datasourceNameToIdMap = new HashMap<>();
// Artifact dependent
// This attribute is re-usable across artifacts according to the needs
Map<String, String> pageOrModuleNewNameToOldName;
/**
* Attribute used to carry objects specific to the context of the Artifacts.
* In case of application it carries the NewPage entity
* In case of packages it would carry modules
*/
Map<String, ? extends BranchAwareDomain> pageOrModuleMap;
// Artifact dependent and common
List<CustomJSLibContextDTO> installedJsLibsList;
Map<String, String> newPageNameToOldPageNameMap;
Map<String, NewPage> pageNameMap;
ImportActionResultDTO actionResultDTO;
ImportActionCollectionResultDTO actionCollectionResultDTO;
ImportedActionAndCollectionMapsDTO actionAndCollectionMapsDTO = new ImportedActionAndCollectionMapsDTO();
// This is being used to carry the resources from ArtifactExchangeJson
Map<String, Object> resourceStoreFromArtifactExchangeJson = new HashMap<>();
}

View File

@ -136,7 +136,8 @@ public class PartialExportServiceCEImpl implements PartialExportServiceCE {
branchedPageId,
partialExportFileDTO.getActionCollectionList(),
applicationJson,
mappedResourcesDTO)
mappedResourcesDTO,
branchName)
.then(Mono.just(branchedPageId));
}
return Mono.just(branchedPageId);
@ -148,7 +149,8 @@ public class PartialExportServiceCEImpl implements PartialExportServiceCE {
branchedPageId,
partialExportFileDTO.getActionList(),
applicationJson,
mappedResourcesDTO)
mappedResourcesDTO,
branchName)
.then(Mono.just(branchedPageId));
}
return Mono.just(branchedPageId);
@ -212,11 +214,17 @@ public class PartialExportServiceCEImpl implements PartialExportServiceCE {
String pageId,
List<String> validActions,
ApplicationJson applicationJson,
MappedExportableResourcesDTO mappedResourcesDTO) {
MappedExportableResourcesDTO mappedResourcesDTO,
String branchName) {
return newActionService.findByPageId(pageId).collectList().flatMap(actions -> {
// For git connected app, the filtering has to be done on the default action id
// since the client is not aware of the branched resource id
List<NewAction> updatedActionList = actions.stream()
.filter(action -> validActions.contains(action.getId()))
.filter(action -> branchName != null
? validActions.contains(action.getDefaultResources().getActionId())
: validActions.contains(action.getId()))
.toList();
// Map name to id for exportable entities
newActionExportableService.mapNameToIdForExportableEntities(mappedResourcesDTO, updatedActionList);
// Make it exportable by removing the ids
@ -232,10 +240,16 @@ public class PartialExportServiceCEImpl implements PartialExportServiceCE {
String pageId,
List<String> validActions,
ApplicationJson applicationJson,
MappedExportableResourcesDTO mappedResourcesDTO) {
MappedExportableResourcesDTO mappedResourcesDTO,
String branchName) {
return actionCollectionService.findByPageId(pageId).collectList().flatMap(actionCollections -> {
// For git connected app, the filtering has to be done on the default actionCollection id
// since the client is not aware of the branched resource id
List<ActionCollection> updatedActionCollectionList = actionCollections.stream()
.filter(actionCollection -> validActions.contains(actionCollection.getId()))
.filter(actionCollection -> branchName != null
? validActions.contains(
actionCollection.getDefaultResources().getCollectionId())
: validActions.contains(actionCollection.getId()))
.toList();
// Map name to id for exportable entities
actionCollectionExportableService.mapNameToIdForExportableEntities(

View File

@ -9,6 +9,7 @@ import com.appsmith.server.domains.NewPage;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.solutions.ActionPermission;
import com.appsmith.server.solutions.ApplicationPermission;
import com.appsmith.server.solutions.ArtifactPermission;
import com.appsmith.server.solutions.DatasourcePermission;
import com.appsmith.server.solutions.PagePermission;
import com.appsmith.server.solutions.WorkspacePermission;
@ -39,9 +40,9 @@ import java.util.Set;
*/
@AllArgsConstructor
@Getter
public class ImportApplicationPermissionProvider {
public class ImportArtifactPermissionProvider {
@Getter(AccessLevel.NONE)
private final ApplicationPermission applicationPermission;
private final ArtifactPermission artifactPermission;
@Getter(AccessLevel.NONE)
private final PagePermission pagePermission;
@ -124,7 +125,7 @@ public class ImportApplicationPermissionProvider {
if (!permissionRequiredToCreatePage) {
return true;
}
return hasPermission(applicationPermission.getPageCreatePermission(), application);
return hasPermission(((ApplicationPermission) artifactPermission).getPageCreatePermission(), application);
}
public boolean canCreateAction(NewPage page) {
@ -142,19 +143,19 @@ public class ImportApplicationPermissionProvider {
}
public static Builder builder(
ApplicationPermission applicationPermission,
ArtifactPermission artifactPermission,
PagePermission pagePermission,
ActionPermission actionPermission,
DatasourcePermission datasourcePermission,
WorkspacePermission workspacePermission) {
return new Builder(
applicationPermission, pagePermission, actionPermission, datasourcePermission, workspacePermission);
artifactPermission, pagePermission, actionPermission, datasourcePermission, workspacePermission);
}
@Setter
@Accessors(chain = true, fluent = true)
public static class Builder {
private final ApplicationPermission applicationPermission;
private final ArtifactPermission artifactPermission;
private final PagePermission pagePermission;
private final ActionPermission actionPermission;
private final DatasourcePermission datasourcePermission;
@ -173,12 +174,12 @@ public class ImportApplicationPermissionProvider {
private boolean permissionRequiredToEditDatasource;
private Builder(
ApplicationPermission applicationPermission,
ArtifactPermission artifactPermission,
PagePermission pagePermission,
ActionPermission actionPermission,
DatasourcePermission datasourcePermission,
WorkspacePermission workspacePermission) {
this.applicationPermission = applicationPermission;
this.artifactPermission = artifactPermission;
this.pagePermission = pagePermission;
this.actionPermission = actionPermission;
this.datasourcePermission = datasourcePermission;
@ -195,11 +196,11 @@ public class ImportApplicationPermissionProvider {
return this;
}
public ImportApplicationPermissionProvider build() {
public ImportArtifactPermissionProvider build() {
// IMPORTANT: make sure that we've added unit tests for all the properties.
// Otherwise, we may end up passing value of one attribute of same type to another.
return new ImportApplicationPermissionProvider(
applicationPermission,
return new ImportArtifactPermissionProvider(
artifactPermission,
pagePermission,
actionPermission,
datasourcePermission,

View File

@ -0,0 +1,3 @@
package com.appsmith.server.imports.importable;
public interface ImportService extends ImportServiceCE {}

View File

@ -0,0 +1,96 @@
package com.appsmith.server.imports.importable;
import com.appsmith.server.constants.ArtifactJsonType;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportableArtifactDTO;
import com.appsmith.server.imports.internal.ContextBasedImportService;
import org.springframework.http.codec.multipart.Part;
import reactor.core.publisher.Mono;
import java.util.List;
public interface ImportServiceCE {
/**
* This method provides the importService specific to the artifact based on the ArtifactJsonType.
* time complexity is O(1), as the map from which the service is being passes is pre-computed
* @param artifactExchangeJson : Entity Json which is implementing the artifactExchangeJson
* @return import-service which is implementing the ContextBasedServiceInterface
*/
ContextBasedImportService<
? extends ImportableArtifact, ? extends ImportableArtifactDTO, ? extends ArtifactExchangeJson>
getContextBasedImportService(ArtifactExchangeJson artifactExchangeJson);
/**
* This method provides the importService specific to the artifact based on the ArtifactJsonType.
* time complexity is O(1), as the map from which the service is being passes is pre-computed
* @param artifactJsonType : Type of Json serialisation
* @return import-service which is implementing the ContextBasedServiceInterface
*/
ContextBasedImportService<
? extends ImportableArtifact, ? extends ImportableArtifactDTO, ? extends ArtifactExchangeJson>
getContextBasedImportService(ArtifactJsonType artifactJsonType);
/**
* This method takes a file part and makes a Json entity which implements the ArtifactExchangeJson interface
*
* @param filePart : filePart from which the contents would be made
* @param artifactJsonType : type of the dataExchangeJson
* @return : Json entity which implements ArtifactExchangeJson
*/
Mono<? extends ArtifactExchangeJson> extractArtifactExchangeJson(Part filePart, ArtifactJsonType artifactJsonType);
/**
* Hydrates an ImportableArtifact within the specified workspace by saving the provided JSON file.
*
* @param filePart The filePart representing the ImportableArtifact object to be saved.
* The ImportableArtifact implements the ImportableArtifact interface.
* @param workspaceId The identifier for the destination workspace.
* @param artifactId
* @param artifactJsonType
*/
Mono<? extends ImportableArtifactDTO> extractArtifactExchangeJsonAndSaveArtifact(
Part filePart, String workspaceId, String artifactId, ArtifactJsonType artifactJsonType);
/**
* Saves the provided ArtifactExchangeJson within the specified workspace.
*
* @param workspaceId The identifier for the destination workspace.
* @param artifactExchangeJson The JSON file representing the ImportableArtifact object to be saved.
* The ImportableArtifact implements the ImportableArtifact interface.
*/
Mono<? extends ImportableArtifact> importNewArtifactInWorkspaceFromJson(
String workspaceId, ArtifactExchangeJson artifactExchangeJson);
Mono<? extends ImportableArtifact> updateNonGitConnectedArtifactFromJson(
String workspaceId, String artifactId, ArtifactExchangeJson artifactExchangeJson);
/**
* Updates an existing ImportableArtifact connected to Git within the specified workspace.
*
* @param workspaceId The identifier for the destination workspace.
* @param artifactId The ImportableArtifact id that needs to be updated with the new resources.
* @param artifactExchangeJson The ImportableArtifact JSON containing necessary information to update the ImportableArtifact.
* @param branchName The name of the Git branch. Set to null if not connected to Git.
* @return The updated ImportableArtifact stored in the database.
*/
Mono<? extends ImportableArtifact> importArtifactInWorkspaceFromGit(
String workspaceId, String artifactId, ArtifactExchangeJson artifactExchangeJson, String branchName);
Mono<? extends ImportableArtifact> mergeArtifactExchangeJsonWithImportableArtifact(
String workspaceId,
String artifactId,
String branchName,
ArtifactExchangeJson artifactExchangeJson,
List<String> entitiesToImport);
Mono<? extends ImportableArtifact> restoreSnapshot(
String workspaceId, ArtifactExchangeJson artifactExchangeJson, String artifactId, String branchName);
Mono<? extends ImportableArtifactDTO> getArtifactImportDTO(
String workspaceId,
String artifactId,
ImportableArtifact importableArtifact,
ArtifactExchangeJson artifactExchangeJson);
}

View File

@ -2,8 +2,10 @@ package com.appsmith.server.imports.importable;
import com.appsmith.external.models.BaseDomain;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.ApplicationJson;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import reactor.core.publisher.Mono;
@ -25,4 +27,15 @@ public interface ImportableServiceCE<T extends BaseDomain> {
boolean isPartialImport) {
return null;
}
default Mono<Void> importEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importContextMono,
ArtifactExchangeJson importableContextJson,
boolean isPartialImport,
boolean isContextAgnostic) {
return null;
}
}

View File

@ -0,0 +1,9 @@
package com.appsmith.server.imports.internal;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportableArtifactDTO;
public interface ContextBasedImportService<
T extends ImportableArtifact, U extends ImportableArtifactDTO, V extends ArtifactExchangeJson>
extends ContextBasedImportServiceCE<T, U, V> {}

View File

@ -0,0 +1,153 @@
package com.appsmith.server.imports.internal;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportableArtifactDTO;
import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.Set;
public interface ContextBasedImportServiceCE<
T extends ImportableArtifact, U extends ImportableArtifactDTO, V extends ArtifactExchangeJson> {
V extractArtifactExchangeJson(String jsonString);
ImportArtifactPermissionProvider getImportArtifactPermissionProviderForImportingArtifact(
Set<String> userPermissions);
ImportArtifactPermissionProvider getImportArtifactPermissionProviderForUpdatingArtifact(
Set<String> userPermissions);
ImportArtifactPermissionProvider getImportArtifactPermissionProviderForConnectingToGit(Set<String> userPermissions);
ImportArtifactPermissionProvider getImportArtifactPermissionProviderForRestoringSnapshot(
Set<String> userPermissions);
ImportArtifactPermissionProvider getImportArtifactPermissionProviderForMergingJsonWithArtifact(
Set<String> userPermissions);
/**
* this method creates updates the entities which is to be imported in context to the artifact
*
* @param artifactExchangeJson : json for the artifact which is going to be imported
* @param entitiesToImport : list of names of entities which is going to be imported
*/
default void updateArtifactExchangeJsonWithEntitiesToBeConsumed(
ArtifactExchangeJson artifactExchangeJson, List<String> entitiesToImport) {}
/**
* this method sets the names to null before the update to avoid conflict
*
* @param artifactId
* @param artifactExchangeJson
*/
void setJsonArtifactNameToNullBeforeUpdate(String artifactId, ArtifactExchangeJson artifactExchangeJson);
Mono<U> getImportableArtifactDTO(String workspaceId, String artifactId, ImportableArtifact importableArtifact);
/**
* This method sets the client & server schema version to artifacts which is inside JSON from the clientSchemaVersion
* & serverSchemaVersion attribute from ArtifactExchangeJson
* @param artifactExchangeJson : ArtifactExchangeJson created from file part while import flow
*/
void syncClientAndSchemaVersion(ArtifactExchangeJson artifactExchangeJson);
/**
* This method saves the context from the import json for the first time after dehydrating all the details which can cause conflicts
*
* @param importableArtifact
* @param importingMetaDTO
* @param mappedImportableResourcesDTO
* @param currentUserMono
* @return
*/
Mono<T> updateAndSaveArtifactInContext(
ImportableArtifact importableArtifact,
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<User> currentUserMono);
/**
* update importable entities with the context references post creation of context in db
* @param importableContext
* @param mappedImportableResourcesDTO
* @param importingMetaDTO
* @return
*/
Mono<T> updateImportableEntities(
ImportableArtifact importableContext,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
ImportingMetaDTO importingMetaDTO);
/**
* Update the artifact after the entities has been created
* @param importableArtifact : the artifact which has to be updated
* @return
*/
Mono<T> updateImportableArtifact(ImportableArtifact importableArtifact);
Map<String, Object> createImportAnalyticsData(
ArtifactExchangeJson artifactExchangeJson, ImportableArtifact importableArtifact);
/**
* @param importingMetaDTO
* @param mappedImportableResourcesDTO
* @param workspaceMono
* @param importableArtifactMono
* @param artifactExchangeJson
* @return
*/
Flux<Void> generateArtifactContextIndependentImportableEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importableArtifactMono,
ArtifactExchangeJson artifactExchangeJson);
/**
* @param importingMetaDTO
* @param mappedImportableResourcesDTO
* @param workspaceMono
* @param importableArtifactMono
* @param artifactExchangeJson
* @return
*/
Flux<Void> generateArtifactContextDependentImportableEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importableArtifactMono,
ArtifactExchangeJson artifactExchangeJson);
/**
* Add entities which are specific to the artifact. i.e. customJsLib
* @param artifactExchangeJson
* @param importingMetaDTO
* @param mappedImportableResourcesDTO
* @return
*/
Mono<Void> generateArtifactSpecificImportableEntities(
ArtifactExchangeJson artifactExchangeJson,
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO);
Mono<Boolean> isArtifactConnectedToGit(String artifactId);
String validateArtifactSpecificFields(ArtifactExchangeJson artifactExchangeJson);
/**
* This map keeps constants which are specific to the contexts i.e. Application, packages.
* which is parallel to other Artifacts.
* i.e. Artifact --> Application, Packages
* i.e. ID --> applicationId, packageId
*/
Map<String, String> getArtifactSpecificConstantsMap();
}

View File

@ -24,7 +24,7 @@ import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.ImportExportUtils;
import com.appsmith.server.helpers.ce.ImportApplicationPermissionProvider;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import com.appsmith.server.imports.importable.ImportableService;
import com.appsmith.server.layouts.UpdateLayoutService;
import com.appsmith.server.migrations.ApplicationVersion;
@ -155,9 +155,9 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
});
}
private Mono<ImportApplicationPermissionProvider> getPermissionProviderForUpdateNonGitConnectedAppFromJson() {
private Mono<ImportArtifactPermissionProvider> getPermissionProviderForUpdateNonGitConnectedAppFromJson() {
return permissionGroupRepository.getCurrentUserPermissionGroups().map(permissionGroups -> {
ImportApplicationPermissionProvider permissionProvider = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider permissionProvider = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
@ -213,7 +213,7 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
return getPermissionProviderForUpdateNonGitConnectedAppFromJson()
.zipWith(permissionGroupIdsMono)
.flatMap(tuple2 -> {
ImportApplicationPermissionProvider permissionProvider = tuple2.getT1();
ImportArtifactPermissionProvider permissionProvider = tuple2.getT1();
Set<String> permissionGroups = tuple2.getT2();
if (!StringUtils.isEmpty(applicationId)
@ -266,7 +266,7 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
}
return permissionGroupRepository.getCurrentUserPermissionGroups().flatMap(userPermissionGroups -> {
ImportApplicationPermissionProvider permissionProvider = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider permissionProvider = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
@ -304,7 +304,7 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
* Sync is a system level operation to get the latest code from Git. If the user does not have some
* permissions on the Application e.g. create page, that'll be checked when the user tries to create a page.
*/
ImportApplicationPermissionProvider permissionProvider = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider permissionProvider = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
@ -332,7 +332,7 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
* Only permission required is to edit the application.
*/
return permissionGroupRepository.getCurrentUserPermissionGroups().flatMap(userPermissionGroups -> {
ImportApplicationPermissionProvider permissionProvider = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider permissionProvider = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
@ -406,29 +406,29 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
return application;
});
if (StringUtils.isEmpty(importingMetaDTO.getApplicationId())) {
if (StringUtils.isEmpty(importingMetaDTO.getArtifactId())) {
importApplicationMono = importApplicationMono.flatMap(application -> {
return applicationPageService.createOrUpdateSuffixedApplication(application, application.getName(), 0);
});
} else {
Mono<Application> existingApplicationMono = applicationService
.findById(
importingMetaDTO.getApplicationId(),
importingMetaDTO.getArtifactId(),
importingMetaDTO.getPermissionProvider().getRequiredPermissionOnTargetApplication())
.switchIfEmpty(Mono.defer(() -> {
log.error(
"No application found with id: {} and permission: {}",
importingMetaDTO.getApplicationId(),
importingMetaDTO.getArtifactId(),
importingMetaDTO.getPermissionProvider().getRequiredPermissionOnTargetApplication());
return Mono.error(new AppsmithException(
AppsmithError.ACL_NO_RESOURCE_FOUND,
FieldName.APPLICATION,
importingMetaDTO.getApplicationId()));
importingMetaDTO.getArtifactId()));
}))
.cache();
// this can be a git sync, import page from template, update app with json, restore snapshot
if (importingMetaDTO.getAppendToApp()) { // we don't need to do anything with the imported application
if (importingMetaDTO.getAppendToArtifact()) { // we don't need to do anything with the imported application
importApplicationMono = existingApplicationMono;
} else {
importApplicationMono = Mono.zip(importApplicationMono, existingApplicationMono)
@ -500,7 +500,7 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
String applicationId,
String branchName,
boolean appendToApp,
ImportApplicationPermissionProvider permissionProvider,
ImportArtifactPermissionProvider permissionProvider,
Set<String> permissionGroups) {
/*
1. Migrate resource to latest schema
@ -905,7 +905,7 @@ public class ImportApplicationServiceCEImpl implements ImportApplicationServiceC
}
return permissionGroupRepository.getCurrentUserPermissionGroups().flatMap(userPermissionGroups -> {
ImportApplicationPermissionProvider permissionProvider = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider permissionProvider = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,

View File

@ -0,0 +1,741 @@
package com.appsmith.server.imports.internal;
import com.appsmith.external.constants.AnalyticsEvents;
import com.appsmith.external.helpers.Stopwatch;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.applications.imports.ApplicationImportService;
import com.appsmith.server.constants.ArtifactJsonType;
import com.appsmith.server.constants.FieldName;
import com.appsmith.server.domains.CustomJSLib;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.User;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportableArtifactDTO;
import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.ImportExportUtils;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import com.appsmith.server.imports.importable.ImportServiceCE;
import com.appsmith.server.imports.importable.ImportableService;
import com.appsmith.server.migrations.ArtifactSchemaMigration;
import com.appsmith.server.repositories.PermissionGroupRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.WorkspaceService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.MediaType;
import org.springframework.http.codec.multipart.Part;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.appsmith.server.constants.ArtifactJsonType.APPLICATION;
@Slf4j
public class ImportServiceCEImpl implements ImportServiceCE {
public static final Set<MediaType> ALLOWED_CONTENT_TYPES = Set.of(MediaType.APPLICATION_JSON);
private static final String INVALID_JSON_FILE = "invalid json file";
private final ApplicationImportService applicationImportService;
private final SessionUserService sessionUserService;
private final WorkspaceService workspaceService;
private final ImportableService<CustomJSLib> customJSLibImportableService;
private final PermissionGroupRepository permissionGroupRepository;
private final TransactionalOperator transactionalOperator;
private final AnalyticsService analyticsService;
private final ImportableService<Plugin> pluginImportableService;
private final ImportableService<Datasource> datasourceImportableService;
private final ImportableService<Theme> themeImportableService;
private final Map<ArtifactJsonType, ContextBasedImportService<?, ?, ?>> serviceFactory = new HashMap<>();
public ImportServiceCEImpl(
ApplicationImportService applicationImportService,
SessionUserService sessionUserService,
WorkspaceService workspaceService,
ImportableService<CustomJSLib> customJSLibImportableService,
PermissionGroupRepository permissionGroupRepository,
TransactionalOperator transactionalOperator,
AnalyticsService analyticsService,
ImportableService<Plugin> pluginImportableService,
ImportableService<Datasource> datasourceImportableService,
ImportableService<Theme> themeImportableService) {
this.applicationImportService = applicationImportService;
this.workspaceService = workspaceService;
this.sessionUserService = sessionUserService;
this.customJSLibImportableService = customJSLibImportableService;
this.permissionGroupRepository = permissionGroupRepository;
this.transactionalOperator = transactionalOperator;
this.analyticsService = analyticsService;
this.pluginImportableService = pluginImportableService;
this.datasourceImportableService = datasourceImportableService;
this.themeImportableService = themeImportableService;
serviceFactory.put(APPLICATION, applicationImportService);
}
/**
* This method provides the importService specific to the artifact based on the ArtifactJsonType.
* time complexity is O(1), as the map from which the service is being passes is pre-computed
* @param artifactExchangeJson : Entity Json which is implementing the artifactExchangeJson
* @return import-service which is implementing the ContextBasedServiceInterface
*/
@Override
public ContextBasedImportService<
? extends ImportableArtifact, ? extends ImportableArtifactDTO, ? extends ArtifactExchangeJson>
getContextBasedImportService(ArtifactExchangeJson artifactExchangeJson) {
return getContextBasedImportService(artifactExchangeJson.getArtifactJsonType());
}
/**
* This method provides the importService specific to the artifact based on the ArtifactJsonType.
* time complexity is O(1), as the map from which the service is being passes is pre-computed
* @param artifactJsonType : Type of Json serialisation
* @return import-service which is implementing the ContextBasedServiceInterface
*/
@Override
public ContextBasedImportService<
? extends ImportableArtifact, ? extends ImportableArtifactDTO, ? extends ArtifactExchangeJson>
getContextBasedImportService(ArtifactJsonType artifactJsonType) {
return serviceFactory.getOrDefault(artifactJsonType, applicationImportService);
}
/**
* This method takes a file part and makes a Json entity which implements the ArtifactExchangeJson interface
*
* @param filePart : filePart from which the contents would be made
* @param artifactJsonType : type of the json which is getting imported
* @return : Json entity which implements ArtifactExchangeJson
*/
public Mono<? extends ArtifactExchangeJson> extractArtifactExchangeJson(
Part filePart, ArtifactJsonType artifactJsonType) {
final MediaType contentType = filePart.headers().getContentType();
if (contentType == null || !ALLOWED_CONTENT_TYPES.contains(contentType)) {
log.error("Invalid content type, {}", contentType);
return Mono.error(new AppsmithException(AppsmithError.VALIDATION_FAILURE, INVALID_JSON_FILE));
}
return DataBufferUtils.join(filePart.content())
.map(dataBuffer -> {
byte[] data = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(data);
DataBufferUtils.release(dataBuffer);
return new String(data);
})
.map(jsonString ->
getContextBasedImportService(artifactJsonType).extractArtifactExchangeJson(jsonString));
}
/**
* Hydrates an ImportableArtifact within the specified workspace by saving the provided JSON file.
*
* @param filePart The filePart representing the ImportableArtifact object to be saved.
* The ImportableArtifact implements the ImportableArtifact interface.
* @param workspaceId The identifier for the destination workspace.
*/
@Override
public Mono<? extends ImportableArtifactDTO> extractArtifactExchangeJsonAndSaveArtifact(
Part filePart, String workspaceId, String artifactId, ArtifactJsonType artifactJsonType) {
if (StringUtils.isEmpty(workspaceId)) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.WORKSPACE_ID));
}
Mono<ImportableArtifactDTO> importedContextMono = extractArtifactExchangeJson(filePart, artifactJsonType)
.zipWhen(contextJson -> {
if (StringUtils.isEmpty(artifactId)) {
return importNewArtifactInWorkspaceFromJson(workspaceId, contextJson);
} else {
return updateNonGitConnectedArtifactFromJson(workspaceId, artifactId, contextJson);
}
})
.flatMap(tuple2 -> {
ImportableArtifact context = tuple2.getT2();
ArtifactExchangeJson artifactExchangeJson = tuple2.getT1();
return getArtifactImportDTO(
context.getWorkspaceId(), context.getId(), context, artifactExchangeJson);
});
return Mono.create(
sink -> importedContextMono.subscribe(sink::success, sink::error, null, sink.currentContext()));
}
/**
* Saves the provided ArtifactExchangeJson within the specified workspace.
*
* @param workspaceId The identifier for the destination workspace.
* @param artifactExchangeJson The JSON file representing the ImportableArtifact object to be saved.
* The ImportableArtifact implements the ImportableArtifact interface.
*/
@Override
public Mono<? extends ImportableArtifact> importNewArtifactInWorkspaceFromJson(
String workspaceId, ArtifactExchangeJson artifactExchangeJson) {
// workspace id must be present and valid
if (StringUtils.isEmpty(workspaceId)) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.WORKSPACE_ID));
}
ContextBasedImportService<?, ?, ?> contextBasedImportService =
getContextBasedImportService(artifactExchangeJson);
return permissionGroupRepository
.getCurrentUserPermissionGroups()
.zipWhen(userPermissionGroup -> {
return Mono.just(contextBasedImportService.getImportArtifactPermissionProviderForImportingArtifact(
userPermissionGroup));
})
.flatMap(tuple2 -> {
Set<String> userPermissionGroup = tuple2.getT1();
ImportArtifactPermissionProvider permissionProvider = tuple2.getT2();
return importArtifactInWorkspace(
workspaceId,
artifactExchangeJson,
null,
null,
false,
permissionProvider,
userPermissionGroup);
});
}
@Override
public Mono<? extends ImportableArtifact> updateNonGitConnectedArtifactFromJson(
String workspaceId, String artifactId, ArtifactExchangeJson artifactExchangeJson) {
ContextBasedImportService<?, ?, ?> contextBasedImportService =
getContextBasedImportService(artifactExchangeJson);
if (StringUtils.isEmpty(workspaceId)) {
return Mono.error(new AppsmithException(AppsmithError.INVALID_PARAMETER, FieldName.WORKSPACE_ID));
}
if (StringUtils.isEmpty(artifactId)) {
// error message according to the context
return Mono.error(new AppsmithException(
AppsmithError.INVALID_PARAMETER,
contextBasedImportService.getArtifactSpecificConstantsMap().get(FieldName.ID)));
}
// Check if the application is connected to git and if it's connected throw exception asking user to update
// app via git ops like pull, merge etc.
Mono<Boolean> isArtifactConnectedToGitMono = Mono.just(Boolean.FALSE);
if (!StringUtils.isEmpty(artifactId)) {
isArtifactConnectedToGitMono = contextBasedImportService.isArtifactConnectedToGit(artifactId);
}
Mono<ImportableArtifact> importedContextMono = isArtifactConnectedToGitMono.flatMap(isConnectedToGit -> {
if (isConnectedToGit) {
return Mono.error(new AppsmithException(
AppsmithError.UNSUPPORTED_IMPORT_OPERATION_FOR_GIT_CONNECTED_APPLICATION));
} else {
contextBasedImportService.setJsonArtifactNameToNullBeforeUpdate(artifactId, artifactExchangeJson);
return permissionGroupRepository
.getCurrentUserPermissionGroups()
.zipWhen(userPermissionGroup -> {
return Mono.just(
contextBasedImportService.getImportArtifactPermissionProviderForUpdatingArtifact(
userPermissionGroup));
})
.flatMap(tuple2 -> {
Set<String> userPermissionGroup = tuple2.getT1();
ImportArtifactPermissionProvider permissionProvider = tuple2.getT2();
return importArtifactInWorkspace(
workspaceId,
artifactExchangeJson,
artifactId,
null,
false,
permissionProvider,
userPermissionGroup);
})
.onErrorResume(error -> {
if (error instanceof AppsmithException) {
return Mono.error(error);
}
return Mono.error(new AppsmithException(
AppsmithError.GENERIC_JSON_IMPORT_ERROR, workspaceId, error.getMessage()));
});
}
});
return Mono.create(
sink -> importedContextMono.subscribe(sink::success, sink::error, null, sink.currentContext()));
}
/**
* Updates an existing ImportableArtifact connected to Git within the specified workspace.
*
* @param workspaceId The identifier for the destination workspace.
* @param artifactId The ImportableArtifact id that needs to be updated with the new resources.
* @param artifactExchangeJson The ImportableArtifact JSON containing necessary information to update the ImportableArtifact.
* @param branchName The name of the Git branch. Set to null if not connected to Git.
* @return The updated ImportableArtifact stored in the database.
*/
@Override
public Mono<? extends ImportableArtifact> importArtifactInWorkspaceFromGit(
String workspaceId, String artifactId, ArtifactExchangeJson artifactExchangeJson, String branchName) {
ContextBasedImportService<?, ?, ?> contextBasedImportService =
getContextBasedImportService(artifactExchangeJson);
return permissionGroupRepository
.getCurrentUserPermissionGroups()
.zipWhen(userPermissionGroups -> {
return Mono.just(contextBasedImportService.getImportArtifactPermissionProviderForConnectingToGit(
userPermissionGroups));
})
.flatMap(tuple2 -> {
Set<String> userPermissionGroup = tuple2.getT1();
ImportArtifactPermissionProvider artifactPermissionProvider = tuple2.getT2();
return importArtifactInWorkspace(
workspaceId,
artifactExchangeJson,
artifactId,
branchName,
false,
artifactPermissionProvider,
userPermissionGroup);
});
}
@Override
public Mono<? extends ImportableArtifact> restoreSnapshot(
String workspaceId, ArtifactExchangeJson artifactExchangeJson, String artifactId, String branchName) {
/**
* Like Git, restore snapshot is a system level operation. So, we're not checking for any permissions here.
* Only permission required is to edit the artifact.
*/
ContextBasedImportService<?, ?, ?> contextBasedImportService =
getContextBasedImportService(artifactExchangeJson);
return permissionGroupRepository
.getCurrentUserPermissionGroups()
.zipWhen(userPermissionGroups -> {
return Mono.just(contextBasedImportService.getImportArtifactPermissionProviderForRestoringSnapshot(
userPermissionGroups));
})
.flatMap(tuple2 -> {
Set<String> userPermissionGroup = tuple2.getT1();
ImportArtifactPermissionProvider importArtifactPermissionProvider = tuple2.getT2();
return importArtifactInWorkspace(
workspaceId,
artifactExchangeJson,
artifactId,
branchName,
false,
importArtifactPermissionProvider,
userPermissionGroup);
});
}
/**
* This function will take the Json filePart and saves the artifact (likely an application) in workspace.
* It'll not create a new ImportableArtifact, it'll update the existing ImportableArtifact by appending the pages to the ImportableArtifact.
* The destination ImportableArtifact will be as it is, only the pages will be appended.
* This method will likely be only applicable for applications
*
* @param workspaceId ID in which the artifact is to be merged
* @param artifactId default ID of the importableArtifact where this artifactExchangeJson is going to get merged with
* @param branchName name of the branch of the importableArtifact where this artifactExchangeJson is going to get merged with
* @param artifactExchangeJson artifactExchangeJson of the importableArtifact that will be merged to
* @param entitiesToImport Name of the pages that should be merged from the artifactExchangeJson.
* If null or empty, all pages will be merged.
* @return Merged ImportableArtifact
*/
@Override
public Mono<? extends ImportableArtifact> mergeArtifactExchangeJsonWithImportableArtifact(
String workspaceId,
String artifactId,
String branchName,
ArtifactExchangeJson artifactExchangeJson,
List<String> entitiesToImport) {
ContextBasedImportService<?, ?, ?> contextBasedImportService =
getContextBasedImportService(artifactExchangeJson);
contextBasedImportService.updateArtifactExchangeJsonWithEntitiesToBeConsumed(
artifactExchangeJson, entitiesToImport);
return permissionGroupRepository
.getCurrentUserPermissionGroups()
.zipWhen(userPermissionGroups -> {
return Mono.just(
contextBasedImportService.getImportArtifactPermissionProviderForMergingJsonWithArtifact(
userPermissionGroups));
})
.flatMap(tuple2 -> {
Set<String> userPermissionGroup = tuple2.getT1();
ImportArtifactPermissionProvider contextPermissionProvider = tuple2.getT2();
return importArtifactInWorkspace(
workspaceId,
artifactExchangeJson,
artifactId,
branchName,
true,
contextPermissionProvider,
userPermissionGroup);
});
}
/**
* @param workspaceId ID in which the context is to be merged
* @param artifactId default ID of the artifact where this artifactExchangeJson is going to get merged with
* @param importableArtifact the context (i.e. application, packages which is imported)
* @param artifactExchangeJson the Json entity from which the import is happening
* @return ImportableArtifactDTO
*/
@Override
public Mono<? extends ImportableArtifactDTO> getArtifactImportDTO(
String workspaceId,
String artifactId,
ImportableArtifact importableArtifact,
ArtifactExchangeJson artifactExchangeJson) {
return getContextBasedImportService(artifactExchangeJson)
.getImportableArtifactDTO(workspaceId, artifactId, importableArtifact);
}
/**
* Imports an application into MongoDB based on the provided application reference object.
*
* @param workspaceId The identifier for the destination workspace.
* @param artifactExchangeJson The application resource containing necessary information for importing the application.
* @param artifactId The context identifier of the application that needs to be saved with the updated resources.
* @param branchName The name of the branch of the artifact with the specified artifactId.
* @param appendToArtifact Indicates whether artifactExchangeJson will be appended to the existing application or not.
* @return The updated artifact stored in MongoDB.
*/
private Mono<ImportableArtifact> importArtifactInWorkspace(
String workspaceId,
ArtifactExchangeJson artifactExchangeJson,
String artifactId,
String branchName,
boolean appendToArtifact,
ImportArtifactPermissionProvider permissionProvider,
Set<String> permissionGroups) {
ContextBasedImportService<?, ?, ?> contextBasedImportService =
getContextBasedImportService(artifactExchangeJson);
String artifactContextString =
contextBasedImportService.getArtifactSpecificConstantsMap().get(FieldName.ARTIFACT_CONTEXT);
// step 1: Schema Migration
ArtifactExchangeJson importedDoc =
ArtifactSchemaMigration.migrateArtifactExchangeJsonToLatestSchema(artifactExchangeJson);
// Step 2: Validation of context Json
// check for validation error and raise exception if error found
String errorField = validateArtifactExchangeJson(importedDoc);
if (!errorField.isEmpty()) {
log.error("Error in importing {}. Field {} is missing", artifactContextString, errorField);
if (errorField.equals(artifactContextString)) {
return Mono.error(
new AppsmithException(
AppsmithError.VALIDATION_FAILURE,
"Field '" + artifactContextString
+ "' Sorry! Seems like you've imported a page-level json instead of an application. Please use the import within the page."));
}
return Mono.error(new AppsmithException(
AppsmithError.VALIDATION_FAILURE, "Field '" + errorField + "' is missing in the JSON."));
}
ImportingMetaDTO importingMetaDTO = new ImportingMetaDTO(
workspaceId, artifactId, branchName, appendToArtifact, permissionProvider, permissionGroups);
MappedImportableResourcesDTO mappedImportableResourcesDTO = new MappedImportableResourcesDTO();
contextBasedImportService.syncClientAndSchemaVersion(importedDoc);
Mono<Workspace> workspaceMono = workspaceService
.findById(workspaceId, permissionProvider.getRequiredPermissionOnTargetWorkspace())
.switchIfEmpty(Mono.defer(() -> {
log.error(
"No workspace found with id: {} and permission: {}",
workspaceId,
permissionProvider.getRequiredPermissionOnTargetWorkspace());
return Mono.error(new AppsmithException(
AppsmithError.ACL_NO_RESOURCE_FOUND, FieldName.WORKSPACE, workspaceId));
}))
.cache();
Mono<User> currUserMono = sessionUserService.getCurrentUser().cache();
// Start the stopwatch to log the execution time
Stopwatch stopwatch = new Stopwatch(AnalyticsEvents.IMPORT.getEventName());
// this would import customJsLibs for all type of artifacts
Mono<Void> artifactSpecificImportableEntities =
contextBasedImportService.generateArtifactSpecificImportableEntities(
importedDoc, importingMetaDTO, mappedImportableResourcesDTO);
/*
Calling the workspaceMono first to avoid creating multiple mongo transactions.
If the first db call inside a transaction is a Flux, then there's a chance of creating multiple mongo
transactions which will lead to NoSuchTransaction exception.
*/
final Mono<? extends ImportableArtifact> importedArtifactMono = workspaceMono
.then(Mono.defer(() -> artifactSpecificImportableEntities))
.then(Mono.defer(() -> contextBasedImportService.updateAndSaveArtifactInContext(
importedDoc.getImportableArtifact(),
importingMetaDTO,
mappedImportableResourcesDTO,
currUserMono)))
.cache();
Mono<? extends ImportableArtifact> importMono = importedArtifactMono
.then(Mono.defer(() -> generateImportableEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedArtifactMono,
importedDoc)))
.then(importedArtifactMono)
.flatMap(importableArtifact -> updateImportableEntities(
contextBasedImportService, importableArtifact, mappedImportableResourcesDTO, importingMetaDTO))
.flatMap(importableArtifact -> updateImportableArtifact(contextBasedImportService, importableArtifact))
.onErrorResume(throwable -> {
String errorMessage = ImportExportUtils.getErrorMessage(throwable);
log.error("Error importing {}. Error: {}", artifactContextString, errorMessage, throwable);
return Mono.error(
new AppsmithException(AppsmithError.GENERIC_JSON_IMPORT_ERROR, workspaceId, errorMessage));
})
.as(transactionalOperator::transactional);
final Mono<? extends ImportableArtifact> resultMono = importMono
.flatMap(importableArtifact -> sendImportedContextAnalyticsEvent(
contextBasedImportService, importableArtifact, AnalyticsEvents.IMPORT))
.zipWith(currUserMono)
.flatMap(tuple -> {
ImportableArtifact importableArtifact = tuple.getT1();
User user = tuple.getT2();
stopwatch.stopTimer();
stopwatch.stopAndLogTimeInMillis();
return sendImportRelatedAnalyticsEvent(importedDoc, importableArtifact, stopwatch, user);
});
// Import Context is currently a slow API because it needs to import and create context, pages, actions
// and action collection. This process may take time and the client may cancel the request. This leads to the
// flow getting stopped midway producing corrupted objects in DB. The following ensures that even though the
// client may have refreshes the page, the imported context is available and is in sane state.
// To achieve this, we use a synchronous sink which does not take subscription cancellations into account. This
// means that even if the subscriber has cancelled its subscription, the create method still generates its
// event.
return Mono.create(sink -> resultMono.subscribe(sink::success, sink::error, null, sink.currentContext()));
}
/**
* validates whether an artifactExchangeJson contains the required fields or not.
*
* @param importedDoc artifactExchangeJson object that needs to be validated
* @return Name of the field that have error. Empty string otherwise
*/
private String validateArtifactExchangeJson(ArtifactExchangeJson importedDoc) {
// validate common schema things
ContextBasedImportService<?, ?, ?> contextBasedImportService = getContextBasedImportService(importedDoc);
String errorField = "";
if (importedDoc.getImportableArtifact() == null) {
// the error field will be either application, packages, or workflows
errorField =
contextBasedImportService.getArtifactSpecificConstantsMap().get(FieldName.ARTIFACT_CONTEXT);
} else {
// validate contextSpecific-errors
errorField = getContextBasedImportService(importedDoc).validateArtifactSpecificFields(importedDoc);
}
return errorField;
}
/**
* Updates importable entities with the contextDetails.
*
* @param contextBasedImportService
* @param importableArtifact
* @param mappedImportableResourcesDTO
* @param importingMetaDTO
* @return
*/
private Mono<? extends ImportableArtifact> updateImportableEntities(
ContextBasedImportService<?, ?, ?> contextBasedImportService,
ImportableArtifact importableArtifact,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
ImportingMetaDTO importingMetaDTO) {
return contextBasedImportService.updateImportableEntities(
importableArtifact, mappedImportableResourcesDTO, importingMetaDTO);
}
/**
* update the importable context with contextSpecific entities after the entities has been created.
*
* @param contextBasedImportService
* @param importableArtifact
* @return
*/
private Mono<? extends ImportableArtifact> updateImportableArtifact(
ContextBasedImportService<?, ?, ?> contextBasedImportService, ImportableArtifact importableArtifact) {
return contextBasedImportService.updateImportableArtifact(importableArtifact);
}
/**
* This method creates the entities which are mentioned in the contextJson, these are imported in mongodb and then
* the references are added to context
*
* @param importingMetaDTO
* @param mappedImportableResourcesDTO
* @param workspaceMono
* @param importedArtifactMono
* @param artifactExchangeJson
* @return
*/
private Mono<Void> generateImportableEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importedArtifactMono,
ArtifactExchangeJson artifactExchangeJson) {
ContextBasedImportService<?, ?, ?> contextBasedImportService =
getContextBasedImportService(artifactExchangeJson);
Flux<Void> artifactAgnosticImportables = generateArtifactIndependentImportableEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedArtifactMono,
artifactExchangeJson);
Flux<Void> artifactSpecificImportables =
contextBasedImportService.generateArtifactContextIndependentImportableEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedArtifactMono,
artifactExchangeJson);
Flux<Void> artifactContextDependentImportables =
contextBasedImportService.generateArtifactContextDependentImportableEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedArtifactMono,
artifactExchangeJson);
return artifactAgnosticImportables
.thenMany(artifactSpecificImportables)
.thenMany(artifactContextDependentImportables)
.then();
}
/**
* Generate the entities which should be imported irrespective of the context (be it application or packages).
* some of these are plugin and datasource
*
* @param importingMetaDTO
* @param mappedImportableResourcesDTO
* @param workspaceMono
* @param importedArtifactMono
* @param artifactExchangeJson
* @return
*/
protected Flux<Void> generateArtifactIndependentImportableEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importedArtifactMono,
ArtifactExchangeJson artifactExchangeJson) {
// Updates plugin map in importable resources
Mono<Void> installedPluginsMono = pluginImportableService.importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedArtifactMono,
artifactExchangeJson,
false,
true);
// Requires pluginMap to be present in importable resources.
// Updates datasourceNameToIdMap in importable resources.
// Also directly updates required information in DB
Mono<Void> importedDatasourcesMono = installedPluginsMono.then(datasourceImportableService.importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedArtifactMono,
artifactExchangeJson,
false,
true));
// Directly updates required theme information in DB
Mono<Void> importedThemesMono = themeImportableService.importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
importedArtifactMono,
artifactExchangeJson,
false,
true);
return Flux.merge(List.of(importedDatasourcesMono, importedThemesMono));
}
/**
* To send analytics event for import and export of ImportableArtifact i.e. application, packages
*
* @param importableArtifact ImportableArtifact object imported or exported
* @param event AnalyticsEvents event
* @return The ImportableArtifact which is imported or exported
*/
private Mono<? extends ImportableArtifact> sendImportedContextAnalyticsEvent(
ContextBasedImportService<?, ?, ?> contextBasedImportService,
ImportableArtifact importableArtifact,
AnalyticsEvents event) {
// this would result in "application", "packages", or "workflows"
String artifactContextString =
contextBasedImportService.getArtifactSpecificConstantsMap().get(FieldName.ARTIFACT_CONTEXT);
// this would result in "applicationId", "packageId", or "workflowId"
String contextIdString =
contextBasedImportService.getArtifactSpecificConstantsMap().get(FieldName.ID);
return workspaceService.getById(importableArtifact.getWorkspaceId()).flatMap(workspace -> {
final Map<String, Object> eventData =
Map.of(artifactContextString, importableArtifact, FieldName.WORKSPACE, workspace);
final Map<String, Object> data = Map.of(
contextIdString,
importableArtifact.getId(),
FieldName.WORKSPACE_ID,
workspace.getId(),
FieldName.EVENT_DATA,
eventData);
return analyticsService.sendObjectEvent(event, importableArtifact, data);
});
}
/**
* This method deals in data only pertaining to import flow i.e. time taken, entities size, e.t.c
* @param artifactExchangeJson : Json which has been used for importing the artifact
* @param importableArtifact: the artifact which is imported
* @param stopwatch : stopwatch
* @param currentUser : user which has initiated the import
*/
private Mono<ImportableArtifact> sendImportRelatedAnalyticsEvent(
ArtifactExchangeJson artifactExchangeJson,
ImportableArtifact importableArtifact,
Stopwatch stopwatch,
User currentUser) {
Map<String, Object> analyticsData = new HashMap<>(getContextBasedImportService(artifactExchangeJson)
.createImportAnalyticsData(artifactExchangeJson, importableArtifact));
analyticsData.put(FieldName.FLOW_NAME, stopwatch.getFlow());
analyticsData.put("executionTime", stopwatch.getExecutionTime());
return analyticsService
.sendEvent(AnalyticsEvents.UNIT_EXECUTION_TIME.getEventName(), currentUser.getUsername(), analyticsData)
.thenReturn(importableArtifact);
}
}

View File

@ -0,0 +1,43 @@
package com.appsmith.server.imports.internal;
import com.appsmith.external.models.Datasource;
import com.appsmith.server.applications.imports.ApplicationImportService;
import com.appsmith.server.domains.CustomJSLib;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.imports.importable.ImportService;
import com.appsmith.server.imports.importable.ImportableService;
import com.appsmith.server.repositories.PermissionGroupRepository;
import com.appsmith.server.services.AnalyticsService;
import com.appsmith.server.services.SessionUserService;
import com.appsmith.server.services.WorkspaceService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.reactive.TransactionalOperator;
@Component
public class ImportServiceImpl extends ImportServiceCEImpl implements ImportService {
public ImportServiceImpl(
ApplicationImportService applicationImportService,
SessionUserService sessionUserService,
WorkspaceService workspaceService,
ImportableService<CustomJSLib> customJSLibImportableService,
PermissionGroupRepository permissionGroupRepository,
TransactionalOperator transactionalOperator,
AnalyticsService analyticsService,
ImportableService<Plugin> pluginImportableService,
ImportableService<Datasource> datasourceImportableService,
ImportableService<Theme> themeImportableService) {
super(
applicationImportService,
sessionUserService,
workspaceService,
customJSLibImportableService,
permissionGroupRepository,
transactionalOperator,
analyticsService,
pluginImportableService,
datasourceImportableService,
themeImportableService);
}
}

View File

@ -18,7 +18,7 @@ import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.ce.ImportApplicationPermissionProvider;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import com.appsmith.server.imports.importable.ImportableService;
import com.appsmith.server.newpages.base.NewPageService;
import com.appsmith.server.repositories.PermissionGroupRepository;
@ -82,7 +82,7 @@ public class PartialImportServiceCEImpl implements PartialImportServiceCE {
.zipWith(getImportApplicationPermissions())
.flatMap(tuple -> {
ApplicationJson applicationJson = tuple.getT1();
ImportApplicationPermissionProvider permissionProvider = tuple.getT2();
ImportArtifactPermissionProvider permissionProvider = tuple.getT2();
// Set Application in App JSON, remove the pages other than the one to be imported in
// Set the current page in the JSON to be imported
// Debug and get the value from getImportApplicationMono method if any difference
@ -180,9 +180,9 @@ public class PartialImportServiceCEImpl implements PartialImportServiceCE {
});
}
private Mono<ImportApplicationPermissionProvider> getImportApplicationPermissions() {
private Mono<ImportArtifactPermissionProvider> getImportApplicationPermissions() {
return permissionGroupRepository.getCurrentUserPermissionGroups().flatMap(userPermissionGroups -> {
ImportApplicationPermissionProvider permissionProvider = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider permissionProvider = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
@ -261,7 +261,7 @@ public class PartialImportServiceCEImpl implements PartialImportServiceCE {
// update page name reference with newPage
Map<String, NewPage> pageNameMap = new HashMap<>();
pageNameMap.put(pageName, newPage);
mappedImportableResourcesDTO.setPageNameMap(pageNameMap);
mappedImportableResourcesDTO.setPageOrModuleMap(pageNameMap);
if (applicationJson.getActionList() == null) {
return Mono.just(pageName);

View File

@ -0,0 +1,3 @@
package com.appsmith.server.migrations;
public class ArtifactSchemaMigration extends ArtifactSchemaMigrationCE {}

View File

@ -0,0 +1,105 @@
package com.appsmith.server.migrations;
import com.appsmith.server.constants.ArtifactJsonType;
import com.appsmith.server.dtos.ApplicationJson;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.CollectionUtils;
public class ArtifactSchemaMigrationCE {
private static boolean checkCompatibility(ArtifactExchangeJson artifactExchangeJson) {
return (artifactExchangeJson.getClientSchemaVersion() <= JsonSchemaVersions.clientVersion)
&& (artifactExchangeJson.getServerSchemaVersion() <= JsonSchemaVersions.serverVersion);
}
public static ArtifactExchangeJson migrateArtifactExchangeJsonToLatestSchema(
ArtifactExchangeJson artifactExchangeJson) {
// Check if the schema versions are available and set to initial version if not present
Integer serverSchemaVersion = artifactExchangeJson.getServerSchemaVersion() == null
? 0
: artifactExchangeJson.getServerSchemaVersion();
Integer clientSchemaVersion = artifactExchangeJson.getClientSchemaVersion() == null
? 0
: artifactExchangeJson.getClientSchemaVersion();
artifactExchangeJson.setClientSchemaVersion(clientSchemaVersion);
artifactExchangeJson.setServerSchemaVersion(serverSchemaVersion);
if (!checkCompatibility(artifactExchangeJson)) {
throw new AppsmithException(AppsmithError.INCOMPATIBLE_IMPORTED_JSON);
}
migrateClientAndServerSchemas(artifactExchangeJson);
return artifactExchangeJson;
}
/**
* This method migrates the client & server schema of artifactExchangeJson after choosing the right method for migration
* this will likely be overridden in EE codebase for more choices
* @param artifactExchangeJson artifactExchangeJson which is imported
*/
private static void migrateClientAndServerSchemas(ArtifactExchangeJson artifactExchangeJson) {
if (ArtifactJsonType.APPLICATION.equals(artifactExchangeJson.getArtifactJsonType())) {
migrateApplicationJsonClientSchema((ApplicationJson) artifactExchangeJson);
migrateApplicationJsonServerSchema((ApplicationJson) artifactExchangeJson);
}
}
private static ApplicationJson migrateApplicationJsonServerSchema(ApplicationJson applicationJson) {
if (JsonSchemaVersions.serverVersion.equals(applicationJson.getServerSchemaVersion())) {
// No need to run server side migration
return applicationJson;
}
// Run migration linearly
// Updating the schema version after each migration is not required as we are not exiting by breaking the switch
// cases, but this keeps the version number and the migration in sync
switch (applicationJson.getServerSchemaVersion()) {
case 0:
case 1:
// Migration for deprecating archivedAt field in ActionDTO
if (!CollectionUtils.isNullOrEmpty(applicationJson.getActionList())) {
MigrationHelperMethods.updateArchivedAtByDeletedATForActions(applicationJson.getActionList());
}
applicationJson.setServerSchemaVersion(2);
case 2:
// Migration for converting formData elements to one that supports viewType
MigrationHelperMethods.migrateActionFormDataToObject(applicationJson);
applicationJson.setServerSchemaVersion(3);
case 3:
// File structure migration to update git directory structure
applicationJson.setServerSchemaVersion(4);
case 4:
// Remove unwanted fields from DTO and allow serialization for JsonIgnore fields
if (!CollectionUtils.isNullOrEmpty(applicationJson.getPageList())
&& applicationJson.getExportedApplication() != null) {
MigrationHelperMethods.arrangeApplicationPagesAsPerImportedPageOrder(applicationJson);
MigrationHelperMethods.updateMongoEscapedWidget(applicationJson);
}
if (!CollectionUtils.isNullOrEmpty(applicationJson.getActionList())) {
MigrationHelperMethods.updateUserSetOnLoadAction(applicationJson);
}
applicationJson.setServerSchemaVersion(5);
case 5:
MigrationHelperMethods.migrateGoogleSheetsActionsToUqi(applicationJson);
applicationJson.setServerSchemaVersion(6);
case 6:
MigrationHelperMethods.ensureXmlParserPresenceInCustomJsLibList(applicationJson);
applicationJson.setServerSchemaVersion(7);
default:
// Unable to detect the serverSchema
}
return applicationJson;
}
private static ApplicationJson migrateApplicationJsonClientSchema(ApplicationJson applicationJson) {
if (JsonSchemaVersions.clientVersion.equals(applicationJson.getClientSchemaVersion())) {
// No need to run client side migration
return applicationJson;
}
// Today server is not responsible to run the client side DSL migration but this can be useful if we start
// supporting this on server side
return applicationJson;
}
}

View File

@ -18,7 +18,7 @@ import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.ce.ImportApplicationPermissionProvider;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import com.appsmith.server.imports.importable.ImportableServiceCE;
import com.appsmith.server.newactions.base.NewActionService;
import com.appsmith.server.repositories.NewActionRepository;
@ -69,12 +69,13 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE<New
List<NewAction> importedNewActionList = applicationJson.getActionList();
Mono<List<NewAction>> importedNewActionMono = Mono.justOrEmpty(importedNewActionList);
if (TRUE.equals(importingMetaDTO.getAppendToApp())) {
if (TRUE.equals(importingMetaDTO.getAppendToArtifact())) {
importedNewActionMono = importedNewActionMono.map(importedNewActionList1 -> {
List<NewPage> importedNewPages = mappedImportableResourcesDTO.getPageNameMap().values().stream()
List<NewPage> importedNewPages = mappedImportableResourcesDTO.getPageOrModuleMap().values().stream()
.distinct()
.map(branchAwareDomain -> (NewPage) branchAwareDomain)
.toList();
Map<String, String> newToOldNameMap = mappedImportableResourcesDTO.getNewPageNameToOldPageNameMap();
Map<String, String> newToOldNameMap = mappedImportableResourcesDTO.getPageOrModuleNewNameToOldName();
for (NewPage newPage : importedNewPages) {
String newPageName = newPage.getUnpublishedPage().getName();
@ -98,8 +99,8 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE<New
// attached to the application:
// Delete the invalid resources (which are not the part of applicationJsonDTO) in
// the git flow only
if (StringUtils.hasText(importingMetaDTO.getApplicationId())
&& !TRUE.equals(importingMetaDTO.getAppendToApp())
if (StringUtils.hasText(importingMetaDTO.getArtifactId())
&& !TRUE.equals(importingMetaDTO.getAppendToArtifact())
&& CollectionUtils.isNotEmpty(importActionResultDTO.getExistingActions())) {
// Remove unwanted actions
Set<String> invalidActionIds = new HashSet<>();
@ -159,8 +160,8 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE<New
// attached to the application:
// Delete the invalid resources (which are not the part of applicationJsonDTO) in
// the git flow only
if (StringUtils.hasText(importingMetaDTO.getApplicationId())
&& !TRUE.equals(importingMetaDTO.getAppendToApp())
if (StringUtils.hasText(importingMetaDTO.getArtifactId())
&& !TRUE.equals(importingMetaDTO.getAppendToArtifact())
&& Boolean.FALSE.equals(isPartialImport)) {
// Remove unwanted action collections
Set<String> invalidCollectionIds = new HashSet<>();
@ -273,7 +274,8 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE<New
unpublishedAction.setId(newAction.getId());
parentPage = updatePageInAction(
unpublishedAction,
mappedImportableResourcesDTO.getPageNameMap(),
(Map<String, NewPage>)
mappedImportableResourcesDTO.getPageOrModuleMap(),
importActionResultDTO.getActionIdMap());
sanitizeDatasourceInActionDTO(
unpublishedAction,
@ -290,7 +292,8 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE<New
}
NewPage publishedActionPage = updatePageInAction(
publishedAction,
mappedImportableResourcesDTO.getPageNameMap(),
(Map<String, NewPage>)
mappedImportableResourcesDTO.getPageOrModuleMap(),
importActionResultDTO.getActionIdMap());
parentPage = parentPage == null ? publishedActionPage : parentPage;
sanitizeDatasourceInActionDTO(
@ -484,7 +487,7 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE<New
NewAction existingAction,
NewAction actionToImport,
String branchName,
ImportApplicationPermissionProvider permissionProvider) {
ImportArtifactPermissionProvider permissionProvider) {
// Since the resource is already present in DB, just update resource
if (!permissionProvider.hasEditPermission(existingAction)) {
log.error("User does not have permission to edit action with id: {}", existingAction.getId());

View File

@ -17,7 +17,7 @@ import com.appsmith.server.exceptions.AppsmithError;
import com.appsmith.server.exceptions.AppsmithException;
import com.appsmith.server.helpers.DefaultResourcesUtils;
import com.appsmith.server.helpers.TextUtils;
import com.appsmith.server.helpers.ce.ImportApplicationPermissionProvider;
import com.appsmith.server.helpers.ce.ImportArtifactPermissionProvider;
import com.appsmith.server.imports.importable.ImportableServiceCE;
import com.appsmith.server.newactions.base.NewActionService;
import com.appsmith.server.newpages.base.NewPageService;
@ -87,7 +87,7 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE<NewPa
importedNewPageList,
existingPagesMono,
applicationMono,
importingMetaDTO.getAppendToApp(),
importingMetaDTO.getAppendToArtifact(),
importingMetaDTO.getBranchName(),
importingMetaDTO.getPermissionProvider(),
mappedImportableResourcesDTO)
@ -100,8 +100,8 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE<NewPa
applicationJson.getExportedApplication(),
pageNameMapMono,
applicationMono,
importingMetaDTO.getAppendToApp(),
importingMetaDTO.getApplicationId(),
importingMetaDTO.getAppendToArtifact(),
importingMetaDTO.getArtifactId(),
existingPagesMono,
importedNewPagesMono,
mappedImportableResourcesDTO)
@ -121,8 +121,9 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE<NewPa
mappedImportableResourcesDTO.getActionAndCollectionMapsDTO();
ImportActionResultDTO importActionResultDTO = mappedImportableResourcesDTO.getActionResultDTO();
List<NewPage> newPages = mappedImportableResourcesDTO.getPageNameMap().values().stream()
List<NewPage> newPages = mappedImportableResourcesDTO.getPageOrModuleMap().values().stream()
.distinct()
.map(branchAwareDomain -> (NewPage) branchAwareDomain)
.toList();
return Flux.fromIterable(newPages)
.flatMap(newPage -> {
@ -170,7 +171,7 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE<NewPa
Mono<Application> importApplicationMono,
boolean appendToApp,
String branchName,
ImportApplicationPermissionProvider permissionProvider,
ImportArtifactPermissionProvider permissionProvider,
MappedImportableResourcesDTO mappedImportableResourcesDTO) {
return Mono.just(importedNewPageList)
.zipWith(existingPagesMono)
@ -184,7 +185,7 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE<NewPa
newToOldNameMap = Map.of();
}
mappedImportableResourcesDTO.setNewPageNameToOldPageNameMap(newToOldNameMap);
mappedImportableResourcesDTO.setPageOrModuleNewNameToOldName(newToOldNameMap);
return Tuples.of(importedNewPages, newToOldNameMap);
})
.zipWith(importApplicationMono)
@ -218,8 +219,24 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE<NewPa
Mono<Tuple2<List<NewPage>, Map<String, String>>> importedNewPagesMono,
MappedImportableResourcesDTO mappedImportableResourcesDTO) {
List<ApplicationPage> editModeApplicationPages = importedApplication.getPages();
List<ApplicationPage> publishedModeApplicationPages = importedApplication.getPublishedPages();
// The access source has been changes because the order of execution has changed.
List<ApplicationPage> editModeApplicationPages = (List<ApplicationPage>) mappedImportableResourcesDTO
.getResourceStoreFromArtifactExchangeJson()
.get(FieldName.UNPUBLISHED);
// this conditional is being placed just for compatibility of the PR #29691
if (CollectionUtils.isEmpty(editModeApplicationPages)) {
editModeApplicationPages = importedApplication.getPages();
}
List<ApplicationPage> publishedModeApplicationPages = (List<ApplicationPage>) mappedImportableResourcesDTO
.getResourceStoreFromArtifactExchangeJson()
.get(FieldName.PUBLISHED);
// this conditional is being placed just for compatibility of the PR #29691
if (CollectionUtils.isEmpty(publishedModeApplicationPages)) {
publishedModeApplicationPages = importedApplication.getPublishedPages();
}
Mono<List<ApplicationPage>> unpublishedPagesMono =
importUnpublishedPages(editModeApplicationPages, appendToApp, applicationMono, importedNewPagesMono);
@ -234,7 +251,7 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE<NewPa
Map<String, NewPage> pageNameMap = objects.getT3();
Application savedApp = objects.getT4();
mappedImportableResourcesDTO.setPageNameMap(pageNameMap);
mappedImportableResourcesDTO.setPageOrModuleMap(pageNameMap);
log.debug("New pages imported for application: {}", savedApp.getId());
Map<ResourceModes, List<ApplicationPage>> applicationPages = new HashMap<>();
@ -370,7 +387,7 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE<NewPa
Application application,
String branchName,
Mono<List<NewPage>> existingPages,
ImportApplicationPermissionProvider permissionProvider) {
ImportArtifactPermissionProvider permissionProvider) {
Map<String, String> oldToNewLayoutIds = new HashMap<>();
pages.forEach(newPage -> {

View File

@ -1,11 +1,13 @@
package com.appsmith.server.plugins.imports;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.domains.Plugin;
import com.appsmith.server.domains.QPlugin;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.domains.WorkspacePlugin;
import com.appsmith.server.dtos.ApplicationJson;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.imports.importable.ImportableServiceCE;
@ -55,4 +57,26 @@ public class PluginImportableServiceCEImpl implements ImportableServiceCE<Plugin
.doOnNext(tuples -> log.debug("time to get plugin map: {}", tuples.getT1()))
.then();
}
@Override
public Mono<Void> importEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importContextMono,
ArtifactExchangeJson importableContextJson,
boolean isPartialImport,
boolean isContextAgnostic) {
return importContextMono.flatMap(importableContext -> {
Application application = (Application) importableContext;
ApplicationJson applicationJson = (ApplicationJson) importableContextJson;
return importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
Mono.just(application),
applicationJson,
isPartialImport);
});
}
}

View File

@ -0,0 +1,5 @@
package com.appsmith.server.solutions;
import com.appsmith.server.solutions.ce.ArtifactPermissionCE;
public interface ArtifactPermission extends ArtifactPermissionCE {}

View File

@ -1,11 +1,9 @@
package com.appsmith.server.solutions.ce;
import com.appsmith.server.acl.AclPermission;
import com.appsmith.server.solutions.ArtifactPermission;
public interface ApplicationPermissionCE {
AclPermission getDeletePermission();
AclPermission getExportPermission();
public interface ApplicationPermissionCE extends ArtifactPermission {
AclPermission getMakePublicPermission();
@ -13,8 +11,6 @@ public interface ApplicationPermissionCE {
AclPermission getPageCreatePermission();
AclPermission getGitConnectPermission();
AclPermission getManageProtectedBranchPermission();
AclPermission getManageDefaultBranchPermission();

View File

@ -0,0 +1,12 @@
package com.appsmith.server.solutions.ce;
import com.appsmith.server.acl.AclPermission;
public interface ArtifactPermissionCE {
AclPermission getDeletePermission();
AclPermission getGitConnectPermission();
AclPermission getExportPermission();
}

View File

@ -2,9 +2,11 @@ package com.appsmith.server.themes.imports;
import com.appsmith.server.applications.base.ApplicationService;
import com.appsmith.server.domains.Application;
import com.appsmith.server.domains.ImportableArtifact;
import com.appsmith.server.domains.Theme;
import com.appsmith.server.domains.Workspace;
import com.appsmith.server.dtos.ApplicationJson;
import com.appsmith.server.dtos.ArtifactExchangeJson;
import com.appsmith.server.dtos.ImportingMetaDTO;
import com.appsmith.server.dtos.MappedImportableResourcesDTO;
import com.appsmith.server.imports.importable.ImportableServiceCE;
@ -55,7 +57,7 @@ public class ThemeImportableServiceCEImpl implements ImportableServiceCE<Theme>
Mono<Application> applicationMono,
ApplicationJson applicationJson,
boolean isPartialImport) {
if (Boolean.TRUE.equals(importingMetaDTO.getAppendToApp())) {
if (Boolean.TRUE.equals(importingMetaDTO.getAppendToArtifact())) {
// appending to existing app, theme should not change
return Mono.empty().then();
}
@ -113,4 +115,26 @@ public class ThemeImportableServiceCEImpl implements ImportableServiceCE<Theme>
}
});
}
@Override
public Mono<Void> importEntities(
ImportingMetaDTO importingMetaDTO,
MappedImportableResourcesDTO mappedImportableResourcesDTO,
Mono<Workspace> workspaceMono,
Mono<? extends ImportableArtifact> importContextMono,
ArtifactExchangeJson importableContextJson,
boolean isPartialImport,
boolean isContextAgnostic) {
return importContextMono.flatMap(importableContext -> {
Application application = (Application) importableContext;
ApplicationJson applicationJson = (ApplicationJson) importableContextJson;
return importEntities(
importingMetaDTO,
mappedImportableResourcesDTO,
workspaceMono,
Mono.just(application),
applicationJson,
isPartialImport);
});
}
}

View File

@ -2,6 +2,8 @@ server.port=${PORT:8080}
# Allow the Spring context to close all active requests before shutting down the server
# Please ref: https://docs.spring.io/spring-boot/docs/2.3.0.RELEASE/reference/html/spring-boot-features.html#boot-features-graceful-shutdown
server.shutdown=graceful
server.max-http-request-header-size=16KB
spring.lifecycle.timeout-per-shutdown-phase=20s
spring.profiles.active=${ACTIVE_PROFILE:production}

View File

@ -12,6 +12,7 @@ import com.appsmith.server.exports.internal.PartialExportService;
import com.appsmith.server.fork.internal.ApplicationForkingService;
import com.appsmith.server.helpers.GitFileUtils;
import com.appsmith.server.helpers.RedisUtils;
import com.appsmith.server.imports.importable.ImportService;
import com.appsmith.server.imports.internal.ImportApplicationService;
import com.appsmith.server.imports.internal.PartialImportService;
import com.appsmith.server.services.AnalyticsService;
@ -58,6 +59,9 @@ public class ApplicationControllerTest {
@MockBean
ImportApplicationService importApplicationService;
@MockBean
ImportService importService;
@MockBean
ExportApplicationService exportApplicationService;

View File

@ -30,7 +30,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SpringBootTest
class ImportApplicationPermissionProviderTest {
class ImportArtifactPermissionProviderTest {
@Autowired
ApplicationPermission applicationPermission;
@ -48,22 +48,21 @@ class ImportApplicationPermissionProviderTest {
@Test
public void testCheckPermissionMethods_WhenNoPermissionProvided_ReturnsTrue() {
ImportApplicationPermissionProvider importApplicationPermissionProvider =
ImportApplicationPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
datasourcePermission,
workspacePermission)
.build();
ImportArtifactPermissionProvider importArtifactPermissionProvider = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
datasourcePermission,
workspacePermission)
.build();
assertTrue(importApplicationPermissionProvider.hasEditPermission(new NewPage()));
assertTrue(importApplicationPermissionProvider.hasEditPermission(new NewAction()));
assertTrue(importApplicationPermissionProvider.hasEditPermission(new Datasource()));
assertTrue(importArtifactPermissionProvider.hasEditPermission(new NewPage()));
assertTrue(importArtifactPermissionProvider.hasEditPermission(new NewAction()));
assertTrue(importArtifactPermissionProvider.hasEditPermission(new Datasource()));
assertTrue(importApplicationPermissionProvider.canCreateDatasource(new Workspace()));
assertTrue(importApplicationPermissionProvider.canCreateAction(new NewPage()));
assertTrue(importApplicationPermissionProvider.canCreatePage(new Application()));
assertTrue(importArtifactPermissionProvider.canCreateDatasource(new Workspace()));
assertTrue(importArtifactPermissionProvider.canCreateAction(new NewPage()));
assertTrue(importArtifactPermissionProvider.canCreatePage(new Application()));
}
@Test
@ -81,7 +80,7 @@ class ImportApplicationPermissionProviderTest {
for (Tuple2<BaseDomain, DomainPermission> domainAndPermission : domainAndPermissionList) {
BaseDomain domain = domainAndPermission.getT1();
// create a permission provider that sets edit permission on the domain
ImportApplicationPermissionProvider provider =
ImportArtifactPermissionProvider provider =
createPermissionProviderForDomainEditPermission(domain, domainAndPermission.getT2());
if (domain instanceof NewPage) {
@ -108,7 +107,7 @@ class ImportApplicationPermissionProviderTest {
for (Tuple2<BaseDomain, AclPermission> domainAndPermission : domainAndPermissionList) {
BaseDomain domain = domainAndPermission.getT1();
// create a permission provider that sets edit permission on the domain
ImportApplicationPermissionProvider provider =
ImportArtifactPermissionProvider provider =
createPermissionProviderForDomainCreatePermission(domain, domainAndPermission.getT2());
if (domain instanceof Application) {
@ -123,7 +122,7 @@ class ImportApplicationPermissionProviderTest {
@Test
public void tesBuilderIsSettingTheCorrectParametersToPermissionProvider() {
ImportApplicationPermissionProvider.Builder builder = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider.Builder builder = ImportArtifactPermissionProvider.builder(
applicationPermission, pagePermission, actionPermission, datasourcePermission, workspacePermission);
assertThat(builder.requiredPermissionOnTargetApplication(applicationPermission.getEditPermission())
@ -147,7 +146,7 @@ class ImportApplicationPermissionProviderTest {
@Test
public void testAllPermissionsRequiredIsSettingAllPermissionsAsRequired() {
ImportApplicationPermissionProvider provider = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider provider = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
@ -176,11 +175,11 @@ class ImportApplicationPermissionProviderTest {
* @param domainPermission
* @return
*/
private ImportApplicationPermissionProvider createPermissionProviderForDomainEditPermission(
private ImportArtifactPermissionProvider createPermissionProviderForDomainEditPermission(
BaseDomain baseDomain, DomainPermission domainPermission) {
setPoliciesToDomain(baseDomain, domainPermission.getEditPermission());
ImportApplicationPermissionProvider.Builder builder = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider.Builder builder = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,
@ -210,11 +209,11 @@ class ImportApplicationPermissionProviderTest {
* @param permission
* @return
*/
private ImportApplicationPermissionProvider createPermissionProviderForDomainCreatePermission(
private ImportArtifactPermissionProvider createPermissionProviderForDomainCreatePermission(
BaseDomain baseDomain, AclPermission permission) {
setPoliciesToDomain(baseDomain, permission);
ImportApplicationPermissionProvider.Builder builder = ImportApplicationPermissionProvider.builder(
ImportArtifactPermissionProvider.Builder builder = ImportArtifactPermissionProvider.builder(
applicationPermission,
pagePermission,
actionPermission,

View File

@ -340,4 +340,75 @@ public class PartialExportServiceTest {
})
.verifyComplete();
}
@Test
@WithUserDetails(value = "api_user")
public void testGetPartialExport_gitConnectedApp_featureBranchResourceExported() {
Mockito.when(pluginService.findAllByIdsWithoutPermission(Mockito.any(), Mockito.anyList()))
.thenReturn(Flux.fromIterable(List.of(installedPlugin, installedJsPlugin)));
Application application =
createGitConnectedApp("testGetPartialExport_gitConnectedApp_featureBranchResourceExported");
// update git branch name for page
PageDTO savedPage = new PageDTO();
savedPage.setName("Page 2");
savedPage.setApplicationId(application.getId());
DefaultResources defaultResources = new DefaultResources();
defaultResources.setApplicationId(application.getId());
defaultResources.setBranchName("master");
savedPage.setDefaultResources(defaultResources);
savedPage = applicationPageService
.createPageWithBranchName(savedPage, "master")
.block();
// Create Action
ActionDTO action = new ActionDTO();
action.setName("validAction");
action.setPageId(savedPage.getId());
action.setExecuteOnLoad(true);
ActionConfiguration actionConfiguration = new ActionConfiguration();
actionConfiguration.setHttpMethod(HttpMethod.GET);
actionConfiguration.setTimeoutInMillisecond("6000");
action.setActionConfiguration(actionConfiguration);
action.setDatasource(datasourceMap.get("DS1"));
DefaultResources defaultResource = new DefaultResources();
defaultResource.setApplicationId(application.getId());
defaultResource.setBranchName("master");
defaultResource.setActionId("testActionId");
action.setDefaultResources(defaultResource);
ActionDTO savedAction =
layoutActionService.createSingleAction(action, Boolean.FALSE).block();
PartialExportFileDTO partialExportFileDTO = new PartialExportFileDTO();
partialExportFileDTO.setDatasourceList(List.of(
datasourceMap.get("DS1").getId(), datasourceMap.get("DS2").getId()));
// For a feature branch the resources in the client always get the default resource id
partialExportFileDTO.setActionList(List.of("testActionId"));
// Get the partial export resources
Mono<ApplicationJson> partialExportFileDTOMono = partialExportService.getPartialExportResources(
application.getId(), savedPage.getId(), "master", partialExportFileDTO);
StepVerifier.create(partialExportFileDTOMono)
.assertNext(applicationJson -> {
assertThat(applicationJson.getDatasourceList().size()).isEqualTo(2);
List<String> dsNames = applicationJson.getDatasourceList().stream()
.map(DatasourceStorage::getName)
.toList();
assertThat(dsNames).containsAll(List.of("DS1", "DS2"));
assertThat(applicationJson.getDatasourceList().get(0).getPluginId())
.isEqualTo("installed-plugin");
assertThat(applicationJson.getDatasourceList().get(1).getPluginId())
.isEqualTo("installed-plugin");
assertThat(applicationJson.getActionList().size()).isEqualTo(1);
NewAction newAction = applicationJson.getActionList().get(0);
assertThat(newAction.getUnpublishedAction().getName()).isEqualTo("validAction");
assertThat(newAction.getUnpublishedAction().getPageId()).isEqualTo("Page 2");
assertThat(newAction.getId()).isEqualTo("Page 2_validAction");
})
.verifyComplete();
}
}