diff --git a/.github/config.json b/.github/config.json index 0189b527aa..dfcec61400 100644 --- a/.github/config.json +++ b/.github/config.json @@ -1 +1 @@ -{"runners":[{"versioning":{"source":"milestones","type":"SemVer"},"prereleaseName":"alpha","issue":{"labels":{"Error Handling":{"conditions":[],"requires":1},"Templates pod":{"conditions":[{"label":"Templates","type":"hasLabel","value":true},{"label":"Community template","type":"hasLabel","value":true},{"label":"Partial-import-export","type":"hasLabel","value":true}],"requires":1},"Team Managers Pod":{"conditions":[{"label":"Settings","type":"hasLabel","value":true},{"label":"Home Page","type":"hasLabel","value":true},{"label":"Realtime Commenting","type":"hasLabel","value":true},{"label":"SSO","type":"hasLabel","value":true},{"label":"Multi User Realtime","type":"hasLabel","value":true},{"label":"RBAC","type":"hasLabel","value":true},{"label":"ABAC","type":"hasLabel","value":true},{"label":"Audit Logs","type":"hasLabel","value":true},{"label":"Multitenancy","type":"hasLabel","value":true},{"label":"Airgap","type":"hasLabel","value":true},{"label":"Enterprise Edition","type":"hasLabel","value":true},{"label":"SCIM","type":"hasLabel","value":true},{"label":"Invite flow","type":"hasLabel","value":true}],"requires":1},"New Developers Pod":{"conditions":[{"label":"Omnibar","type":"hasLabel","value":true},{"label":"Telemetry","type":"hasLabel","value":true},{"label":"Entity Explorer","type":"hasLabel","value":true},{"label":"IDE","type":"hasLabel","value":true},{"label":"Example Apps","type":"hasLabel","value":true},{"label":"i18n","type":"hasLabel","value":true},{"label":"IDE Navigation","type":"hasLabel","value":true},{"label":"Clean URLs","type":"hasLabel","value":true},{"label":"In App Comms","type":"hasLabel","value":true},{"label":"In App Comms","type":"hasLabel","value":true},{"label":"App setting","type":"hasLabel","value":true}],"requires":1},"BE Coders Pod":{"conditions":[{"label":"SAAS Plugins","type":"hasLabel","value":true},{"label":"SAAS Manager App","type":"hasLabel","value":true},{"label":"Data Platform Pod","type":"hasLabel","value":true},{"label":"Integrations Pod","type":"hasLabel","value":true}],"requires":1},"FE Coders Pod":{"conditions":[{"label":"JS Linting & Errors","type":"hasLabel","value":true},{"label":"Debugger","type":"hasLabel","value":true},{"label":"JS Snippets","type":"hasLabel","value":true},{"label":"Autocomplete","type":"hasLabel","value":true},{"label":"Evaluated Value","type":"hasLabel","value":true},{"label":"Slash Command","type":"hasLabel","value":true},{"label":"New JS Function","type":"hasLabel","value":true},{"label":"JS Promises","type":"hasLabel","value":true},{"label":"JS Usability","type":"hasLabel","value":true},{"label":"Code Refactoring","type":"hasLabel","value":true},{"label":"storeValue","type":"hasLabel","value":true},{"label":"OnPageLoad","type":"hasLabel","value":true},{"label":"Framework Functions","type":"hasLabel","value":true},{"label":"Code Editor","type":"hasLabel","value":true},{"label":"JS Objects","type":"hasLabel","value":true},{"label":"JS Evaluation","type":"hasLabel","value":true},{"label":"AST-frontend","type":"hasLabel","value":true},{"label":"Custom JS Libraries","type":"hasLabel","value":true},{"label":"Action Selector","type":"hasLabel","value":true},{"label":"JS Function execution","type":"hasLabel","value":true},{"label":"Widget setter method","type":"hasLabel","value":true},{"label":"Error Handling","type":"hasLabel","value":true}],"requires":1},"App Viewers Pod":{"conditions":[{"label":"Button Widget","type":"hasLabel","value":true},{"label":"Chart Widget","type":"hasLabel","value":true},{"label":"Container Widget","type":"hasLabel","value":true},{"label":"Date Picker Widget","type":"hasLabel","value":true},{"label":"Select Widget","type":"hasLabel","value":true},{"label":"File Picker Widget","type":"hasLabel","value":true},{"label":"Form Widget","type":"hasLabel","value":true},{"label":"Image Widget","type":"hasLabel","value":true},{"label":"Input Widget","type":"hasLabel","value":true},{"label":"List Widget","type":"hasLabel","value":true},{"label":"MultiSelect Widget","type":"hasLabel","value":true},{"label":"Map Widget","type":"hasLabel","value":true},{"label":"Modal Widget","type":"hasLabel","value":true},{"label":"Radio Widget","type":"hasLabel","value":true},{"label":"Rich Text Editor Widget","type":"hasLabel","value":true},{"label":"Tab Widget","type":"hasLabel","value":true},{"label":"Table Widget","type":"hasLabel","value":true},{"label":"Text Widget","type":"hasLabel","value":true},{"label":"Video Widget","type":"hasLabel","value":true},{"label":"iFrame","type":"hasLabel","value":true},{"label":"Menu Button","type":"hasLabel","value":true},{"label":"Rating","type":"hasLabel","value":true},{"label":"Widget Validation","type":"hasLabel","value":true},{"label":"reallabel","type":"hasLabel","value":true},{"label":"New Widget","type":"hasLabel","value":true},{"label":"Switch widget","type":"hasLabel","value":true},{"label":"Audio Widget","type":"hasLabel","value":true},{"label":"Icon Button Widget","type":"hasLabel","value":true},{"label":"Stat Box Widget","type":"hasLabel","value":true},{"label":"Voice Recorder Widget","type":"hasLabel","value":true},{"label":"Calendar Widget","type":"hasLabel","value":true},{"label":"Menu Button Widget","type":"hasLabel","value":true},{"label":"Divider Widget","type":"hasLabel","value":true},{"label":"Rating Widget","type":"hasLabel","value":true},{"label":"App Navigation","type":"hasLabel","value":true},{"label":"View Mode","type":"hasLabel","value":true},{"label":"Widget Property","type":"hasLabel","value":true},{"label":"Document Viewer Widget","type":"hasLabel","value":true},{"label":"Radio Group Widget","type":"hasLabel","value":true},{"label":"Currency Input Widget","type":"hasLabel","value":true},{"label":"TreeSelect","type":"hasLabel","value":true},{"label":"MultiTree Select Widget","type":"hasLabel","value":true},{"label":"Phone Input Widget","type":"hasLabel","value":true},{"label":"JSON Form","type":"hasLabel","value":true},{"label":"All Widgets","type":"hasLabel","value":true},{"label":"Button Group widget","type":"hasLabel","value":true},{"label":"Progress bar widget","type":"hasLabel","value":true},{"label":"Audio Recorder Widget","type":"hasLabel","value":true},{"label":"Camera Widget","type":"hasLabel","value":true},{"label":"Table Widget V2","type":"hasLabel","value":true},{"label":"Branding","type":"hasLabel","value":true},{"label":"Map Chart Widget","type":"hasLabel","value":true},{"label":"Code Scanner Widget","type":"hasLabel","value":true},{"label":"Widget keyboard accessibility","type":"hasLabel","value":true},{"label":"List Widget V2","type":"hasLabel","value":true},{"label":"Slider Widget","type":"hasLabel","value":true},{"label":"One-click Binding","type":"hasLabel","value":true},{"label":"Old widget version","type":"hasLabel","value":true},{"label":"Widget Discoverability","type":"hasLabel","value":true},{"label":"Custom widgets","type":"hasLabel","value":true}],"requires":1},"UI Builders Pod":{"conditions":[{"label":"Property Pane","type":"hasLabel","value":true},{"label":"Pages","type":"hasLabel","value":true},{"label":"Copy Paste","type":"hasLabel","value":true},{"label":"Drag & Drop","type":"hasLabel","value":true},{"label":"Undo/Redo","type":"hasLabel","value":true},{"label":"Widgets Pane","type":"hasLabel","value":true},{"label":"UI Performance","type":"hasLabel","value":true},{"label":"Widget Grouping","type":"hasLabel","value":true},{"label":"Reflow & Resize","type":"hasLabel","value":true},{"label":"Canvas / Grid","type":"hasLabel","value":true},{"label":"Canvas Zooms","type":"hasLabel","value":true},{"label":"Frontend Libraries Upgrade","type":"hasLabel","value":true},{"label":"Auto Height","type":"hasLabel","value":true},{"label":"Responsive Canvas","type":"hasLabel","value":true},{"label":"Responsive Widget","type":"hasLabel","value":true},{"label":"Responsive Viewport","type":"hasLabel","value":true},{"label":"Conversion Algorithm","type":"hasLabel","value":true},{"label":"Spacing","type":"hasLabel","value":true},{"label":"Browser specific","type":"hasLabel","value":true},{"label":"widget vertical alignment","type":"hasLabel","value":true},{"label":"Auto Layout","type":"hasLabel","value":true},{"label":"Fixed layout","type":"hasLabel","value":true},{"label":"Anvil layout","type":"hasLabel","value":true}],"requires":1},"User Education Pod":{"conditions":[{"label":"Content","type":"hasLabel","value":true},{"label":"Documentation","type":"hasLabel","value":true}],"requires":1},"DevOps Pod":{"conditions":[{"label":"Docker","type":"hasLabel","value":true},{"label":"Super Admin","type":"hasLabel","value":true},{"label":"Deployment","type":"hasLabel","value":true},{"label":"K8s","type":"hasLabel","value":true},{"label":"Email Config","type":"hasLabel","value":true},{"label":"Backup & Restore","type":"hasLabel","value":true},{"label":"AWS AMI","type":"hasLabel","value":true},{"label":"Observability","type":"hasLabel","value":true},{"label":"Heroku","type":"hasLabel","value":true},{"label":"New Deployment Mode","type":"hasLabel","value":true}],"requires":1},"Design System Pod":{"conditions":[{"label":"Design System Pod","type":"hasLabel","value":true},{"label":"ADS Component Issue","type":"hasLabel","value":true},{"label":"Keyboard accessibility ","type":"hasLabel","value":true},{"label":"Toggle button","type":"hasLabel","value":true},{"label":"ADS Category Token","type":"hasLabel","value":true},{"label":"ADS Component Documentation","type":"hasLabel","value":true},{"label":"ADS Migration","type":"hasLabel","value":true},{"label":"ADS Deduplication ","type":"hasLabel","value":true},{"label":"ADS Revamp","type":"hasLabel","value":true},{"label":"ADS Deduplication","type":"hasLabel","value":true},{"label":"ADS Unit Test","type":"hasLabel","value":true},{"label":"ADS Components","type":"hasLabel","value":true},{"label":"ADS Grayscale","type":"hasLabel","value":true},{"label":"Design System","type":"hasLabel","value":true},{"label":"ADS Typography","type":"hasLabel","value":true},{"label":"ADS Visual Styles","type":"hasLabel","value":true},{"label":"ADS Component Design","type":"hasLabel","value":true},{"label":"Modal Component","type":"hasLabel","value":true}],"requires":1},"Data Platform Pod":{"conditions":[{"label":"Datasource Environments","type":"hasLabel","value":true},{"label":"Datatype issue","type":"hasLabel","value":true},{"label":"Entity Refactor","type":"hasLabel","value":true},{"label":"Core Query Execution","type":"hasLabel","value":true},{"label":"Query Management","type":"hasLabel","value":true},{"label":"Query Settings","type":"hasLabel","value":true},{"label":"SmartSubstitution","type":"hasLabel","value":true},{"label":"Query Generation","type":"hasLabel","value":true},{"label":"Query performance","type":"hasLabel","value":true},{"label":"Suggested Widgets","type":"hasLabel","value":true},{"label":"Page load executions","type":"hasLabel","value":true},{"label":"DSL Update","type":"hasLabel","value":true},{"label":"AST-backend","type":"hasLabel","value":true},{"label":"Deploy App","type":"hasLabel","value":true},{"label":"File upload issues","type":"hasLabel","value":true},{"label":"Datasources","type":"hasLabel","value":true},{"label":"DocumentDB","type":"hasLabel","value":true},{"label":"Multiple Environments","type":"hasLabel","value":true},{"label":"Platformization","type":"hasLabel","value":true},{"label":"Custom environments","type":"hasLabel","value":true},{"label":"Schema","type":"hasLabel","value":true}],"requires":1},"Integrations Pod":{"conditions":[{"label":"New Datasource","type":"hasLabel","value":true},{"label":"Firestore","type":"hasLabel","value":true},{"label":"Google Sheets","type":"hasLabel","value":true},{"label":"Mongo","type":"hasLabel","value":true},{"label":"Redshift","type":"hasLabel","value":true},{"label":"snowflake","type":"hasLabel","value":true},{"label":"S3","type":"hasLabel","value":true},{"label":"Redis","type":"hasLabel","value":true},{"label":"Postgres","type":"hasLabel","value":true},{"label":"GraphQL Plugin","type":"hasLabel","value":true},{"label":"ArangoDB","type":"hasLabel","value":true},{"label":"MsSQL","type":"hasLabel","value":true},{"label":"REST API plugin","type":"hasLabel","value":true},{"label":"Elastic Search","type":"hasLabel","value":true},{"label":"OAuth","type":"hasLabel","value":true},{"label":"Airtable","type":"hasLabel","value":true},{"label":"CURL","type":"hasLabel","value":true},{"label":"DynamoDB","type":"hasLabel","value":true},{"label":"Zendesk","type":"hasLabel","value":true},{"label":"Hubspot","type":"hasLabel","value":true},{"label":"Query Forms","type":"hasLabel","value":true},{"label":"Twilio","type":"hasLabel","value":true},{"label":"MySQL","type":"hasLabel","value":true},{"label":"Connection pool","type":"hasLabel","value":true},{"label":"MariaDB","type":"hasLabel","value":true},{"label":"Integrations Pod General","type":"hasLabel","value":true},{"label":"SMTP plugin","type":"hasLabel","value":true},{"label":"Oracle SQL DB","type":"hasLabel","value":true},{"label":"Query filter","type":"hasLabel","value":true},{"label":"Activation - datasources","type":"hasLabel","value":true},{"label":"Onboarding","type":"hasLabel","value":true},{"label":"Generate Page","type":"hasLabel","value":true},{"label":"Sniping Mode","type":"hasLabel","value":true},{"label":"Welcome Screen","type":"hasLabel","value":true},{"label":"Login / Signup","type":"hasLabel","value":true}],"requires":1},"Git Pod":{"conditions":[{"label":"Git Version Control","type":"hasLabel","value":true},{"label":"Import-Export-App","type":"hasLabel","value":true},{"label":"Fork App","type":"hasLabel","value":true}],"requires":1},"Mobile Pod":{"conditions":[],"requires":1},"Billing & Usage Pod":{"conditions":[{"label":"CE Instance","type":"hasLabel","value":true},{"label":"Customer Portal","type":"hasLabel","value":true},{"label":"Cloud Services","type":"hasLabel","value":true},{"label":"Billing Integrations","type":"hasLabel","value":true},{"label":"Billing","type":"hasLabel","value":true},{"label":"Self Serve","type":"hasLabel","value":true},{"label":"Enterprise Billing","type":"hasLabel","value":true},{"label":"In-app ramps","type":"hasLabel","value":true},{"label":"Analytics Improvements","type":"hasLabel","value":true},{"label":"Self Serve 1.0","type":"hasLabel","value":true},{"label":"License","type":"hasLabel","value":true},{"label":"1-click upgrade","type":"hasLabel","value":true},{"label":"Appsmith Business Cloud","type":"hasLabel","value":true},{"label":"BE instance","type":"hasLabel","value":true},{"label":"Embedding Apps","type":"hasLabel","value":true},{"label":"TM_BU","type":"hasLabel","value":true},{"label":"Homepage Experience V2","type":"hasLabel","value":true},{"label":"Feature Flagging","type":"hasLabel","value":true},{"label":"Invite flow","type":"hasLabel","value":true},{"label":"Invite users","type":"hasLabel","value":true}],"requires":1},"Performance Pod":{"conditions":[{"label":"Performance","type":"hasLabel","value":true},{"label":"Performance infra","type":"hasLabel","value":true}],"requires":1},"Widget design system":{"conditions":[{"label":"App Theming","type":"hasLabel","value":true},{"label":"Widget Styling","type":"hasLabel","value":true},{"label":"Checkbox Group widget","type":"hasLabel","value":true},{"label":"Checkbox Widget","type":"hasLabel","value":true},{"label":"Checkbox Component","type":"hasLabel","value":true},{"label":"WDS team","type":"hasLabel","value":true},{"label":"Widget design system","type":"hasLabel","value":true}],"requires":1},"IDE Pod":{"conditions":[],"requires":1},"Appsmith Labs":{"conditions":[{"label":"AI","type":"hasLabel","value":true}],"requires":1},"Workflows Pod":{"conditions":[],"requires":1}}},"root":"."}],"labels":{"Tab Widget":{"color":"e2c76c","name":"Tab Widget","description":""},"Dont merge":{"color":"ADB39C","name":"Dont merge","description":""},"Epic":{"color":"3E4B9E","name":"Epic","description":"A zenhub epic that describes a project"},"Menu Button Widget":{"color":"235708","name":"Menu Button Widget","description":"Issues related to Menu Button widget"},"Checkbox Group widget":{"color":"88054d","name":"Checkbox Group widget","description":"Issues related to Checkbox Group Widget"},"Input Widget":{"color":"ae65d8","name":"Input Widget","description":""},"Security":{"color":"99139C","name":"Security","description":""},"QA":{"color":"e2ca68","name":"QA","description":""},"Verified":{"color":"9bf416","name":"Verified","description":""},"Wont Fix":{"color":"ffffff","name":"Wont Fix","description":"This will not be worked on"},"MySQL":{"color":"c9ddc6","name":"MySQL","description":"Issues related to MySQL plugin"},"Development":{"color":"9F8A02","name":"Development","description":""},"Help Wanted":{"color":"008672","name":"Help Wanted","description":"Extra attention is needed"},"Home Page":{"color":"9c0c8e","name":"Home Page","description":"Issues related to the application home page"},"Rating Widget":{"color":"235708","name":"Rating Widget","description":"Issues related to the rating widget"},"Stat Box Widget":{"color":"f1c9ce","name":"Stat Box Widget","description":"Issues related to stat box"},"Enhancement":{"color":"a2eeef","name":"Enhancement","description":"New feature or request"},"Settings":{"color":"f7ff60","name":"Settings","description":"organization, team & user settings"},"Fork App":{"color":"30c76d","name":"Fork App","description":"Issues related to forking apps"},"Container Widget":{"color":"19AD0D","name":"Container Widget","description":"Container widget"},"Papercut":{"color":"B562F6","name":"Papercut","description":""},"Needs Design":{"color":"bfd4f2","name":"Needs Design","description":"needs design or changes to design"},"i18n":{"color":"1799b0","name":"i18n","description":"Represents issues that need to be tackled to handle internationalization"},"Rich Text Editor Widget":{"color":"f72cac","name":"Rich Text Editor Widget","description":""},"Onboarding":{"color":"30c76d","name":"Onboarding","description":"Issues related to onboarding new developers"},"Pages":{"color":"d7fd80","name":"Pages","description":"Issues related to configuring pages"},"skip-changelog":{"color":"06086F","name":"skip-changelog","description":"Adding this label to a PR prevents it from being listed in the changelog"},"Low":{"color":"79e53b","name":"Low","description":"An issue that is neither critical nor breaks a user flow"},"potential-duplicate":{"color":"d3cb2e","name":"potential-duplicate","description":"This label marks issues that are potential duplicates of already open issues"},"Audio Widget":{"color":"447B9A","name":"Audio Widget","description":"Issues related to Audio Widget"},"Firestore":{"color":"8078b0","name":"Firestore","description":"Issues related to the firestore Integration"},"New Widget":{"color":"be4cf2","name":"New Widget","description":"A request for a new widget"},"Modal Widget":{"color":"03846f","name":"Modal Widget","description":""},"UX Improvement":{"color":"f4a089","name":"UX Improvement","description":""},"S3":{"color":"8078b0","name":"S3","description":"Issues related to the S3 plugin"},"Release Blocker":{"color":"5756bf","name":"Release Blocker","description":"This issue must be resolved before the release"},"safari":{"color":"51C6AA","name":"safari","description":"Bugs seen on safari browser"},"Example Apps":{"color":"1799b0","name":"Example Apps","description":"Example apps created for new signups"},"MultiSelect Widget":{"color":"AB62D4","name":"MultiSelect Widget","description":"Issues related to MultiSelect Widget"},"Widget Styling":{"color":"905420","name":"Widget Styling","description":"all about widget styling"},"Calendar Widget":{"color":"8c6644","name":"Calendar Widget","description":""},"Website":{"color":"151720","name":"Website","description":"Related to www.appsmith.com website"},"Low effort":{"color":"8B59F0","name":"Low effort","description":"Something that'll take a few days to build"},"App Viewers Pod":{"color":"cd8ef9","name":"App Viewers Pod","description":"This label assigns issues to the app viewers pod"},"Checkbox Widget":{"color":"88054d","name":"Checkbox Widget","description":""},"Spam":{"color":"620faf","name":"Spam","description":""},"Voice Recorder Widget":{"color":"85bc87","name":"Voice Recorder Widget","description":""},"Select Widget":{"color":"0c669e","name":"Select Widget","description":"Select or dropdown widget"},"Bug":{"color":"d73a4a","name":"Bug","description":"Something isn't working"},"Widget Validation":{"color":"6990BC","name":"Widget Validation","description":"Issues related to widget property validation"},"Generate Page":{"color":"30c76d","name":"Generate Page","description":"Issures related to page generation"},"File Picker Widget":{"color":"6ae4f2","name":"File Picker Widget","description":""},"snowflake":{"color":"8078b0","name":"snowflake","description":"Issues related to the snowflake Integration"},"Automation":{"color":"CCAF60","name":"Automation","description":""},"hotfix":{"color":"BA3F1D","name":"hotfix","description":""},"Team Managers Pod":{"color":"bddb81","name":"Team Managers Pod","description":"Issues that team managers care about for the security and efficiency of their teams"},"Import-Export-App":{"color":"15076d","name":"Import-Export-App","description":"Issues related to importing and exporting apps"},"High effort":{"color":"A7E87B","name":"High effort","description":"Something that'll take more than a month to build"},"Telemetry":{"color":"bc70f9","name":"Telemetry","description":"Issues related to instrumenting appsmith"},"Radio Widget":{"color":"91ef15","name":"Radio Widget","description":""},"Omnibar":{"color":"10b5ce","name":"Omnibar","description":"Issues related to the omnibar for navigation"},"Button Widget":{"color":"34efae","name":"Button Widget","description":""},"Switch widget":{"color":"33A8CE","name":"Switch widget","description":"The switch widget"},"Map Widget":{"color":"7eef7a","name":"Map Widget","description":""},"Task":{"color":"085630","name":"Task","description":"A simple Todo"},"Design System":{"color":"2958a4","name":"Design System","description":"Design system"},"opera":{"color":"C63F5B","name":"opera","description":"Any issues identified on the opera browser"},"Login / Signup":{"color":"30c76d","name":"Login / Signup","description":"Authentication flows"},"Image Widget":{"color":"8de8ad","name":"Image Widget","description":""},"firefox":{"color":"6d56e2","name":"firefox","description":""},"Property Pane":{"color":"b356ff","name":"Property Pane","description":"Issues related to the behaviour of the property pane"},"Deployment":{"color":"93491f","name":"Deployment","description":"Installation process of appsmith"},"Critical":{"color":"9b1b28","name":"Critical","description":"This issue needs immediate attention. Drop everything else"},"IDE":{"color":"61b2ee","name":"IDE","description":"Issues related to the IDE"},"Production":{"color":"b60205","name":"Production","description":""},"Dependencies":{"color":"0366d6","name":"Dependencies","description":"Pull requests that update a dependency file"},"Google Sheets":{"color":"8078b0","name":"Google Sheets","description":"Issues related to Google Sheets"},"Icon Button Widget":{"color":"D319CE","name":"Icon Button Widget","description":"Issues related to the icon button widget"},"Mongo":{"color":"8078b0","name":"Mongo","description":"Issues related to Mongo DB plugin"},"Documentation":{"color":"a8dff7","name":"Documentation","description":"Improvements or additions to documentation"},"TestGap":{"color":"f28253","name":"TestGap","description":"Issues identified for test plan improvement"},"keyboard shortcut":{"color":"0688B6","name":"keyboard shortcut","description":""},"Git Version Control":{"color":"858172","name":"Git Version Control","description":"Issues related to version control"},"Reopen":{"color":"897548","name":"Reopen","description":""},"Redshift":{"color":"8078b0","name":"Redshift","description":"Issues related to the redshift integration"},"Date Picker Widget":{"color":"ef1ce1","name":"Date Picker Widget","description":""},"Entity Explorer":{"color":"a2e2f9","name":"Entity Explorer","description":"Issues related to navigation using the entity explorer"},"JS Linting & Errors":{"color":"E56AA5","name":"JS Linting & Errors","description":"Issues related to JS Linting and errors"},"iFrame":{"color":"3CD1DB","name":"iFrame","description":"Issues related to iFrame"},"Stale":{"color":"ededed","name":"Stale","description":null},"Debugger":{"color":"e79062","name":"Debugger","description":"Issues related to the debugger"},"Quick effort":{"color":"95ED65","name":"Quick effort","description":"Something that'll take a few hours to build"},"Text Widget":{"color":"d130d1","name":"Text Widget","description":""},"Video Widget":{"color":"23dd4b","name":"Video Widget","description":""},"Datasources":{"color":"5052f6","name":"Datasources","description":"Issues related to configuring datasource on appsmith"},"error":{"color":"B66773","name":"error","description":"All issues connected to error messages"},"Form Widget":{"color":"09ed77","name":"Form Widget","description":""},"Needs Triaging":{"color":"e8b851","name":"Needs Triaging","description":"Needs attention from maintainers to triage"},"Autocomplete":{"color":"235708","name":"Autocomplete","description":"Issues related to the autocomplete"},"hacktoberfest":{"color":"0052cc","name":"hacktoberfest","description":"All issues that can be solved by the community during Hacktoberfest"},"Medium effort":{"color":"D31156","name":"Medium effort","description":"Something that'll take more than a week but less than a month to build"},"Release":{"color":"57e5e0","name":"Release","description":""},"High":{"color":"c94d14","name":"High","description":"This issue blocks a user from building or impacts a lot of users"},"UI Performance":{"color":"1799b0","name":"UI Performance","description":"Issues related to UI performance"},"UI Builders Pod":{"color":"517fba","name":"UI Builders Pod","description":"Issues that UI Builders face using appsmith"},"Deploy Preview":{"color":"bfdadc","name":"Deploy Preview","description":"Issues found in Deploy Preview"},"Needs Tests":{"color":"8ee263","name":"Needs Tests","description":"Needs automated tests to assert a feature/bug fix"},"Refactor":{"color":"B96662","name":"Refactor","description":"needs refactoring of code"},"Divider Widget":{"color":"235708","name":"Divider Widget","description":"Issues related to the divider widget"},"Table Widget":{"color":"2eead1","name":"Table Widget","description":""},"Needs More Info":{"color":"e54c10","name":"Needs More Info","description":"Needs additional information"},"Good First Issue":{"color":"7057ff","name":"Good First Issue","description":"Good for newcomers"},"UI Improvement":{"color":"9aeef4","name":"UI Improvement","description":""},"Backend":{"color":"d4c5f9","name":"Backend","description":"This marks the issue or pull request to reference server code"},"Frontend":{"color":"87c7f2","name":"Frontend","description":"This label marks the issue or pull request to reference client code"},"In App Comms":{"name":"In App Comms","description":"Issues around communication with appsmith instances","color":"463cca"},"Chart Widget":{"color":"616ecc","name":"Chart Widget","description":""},"List Widget":{"color":"8508A0","name":"List Widget","description":"Issues related to the list widget"},"Duplicate":{"color":"cfd3d7","name":"Duplicate","description":"This issue or pull request already exists"},"JS Snippets":{"color":"8d62d2","name":"JS Snippets","description":"issues related to JS Snippets"},"Copy Paste":{"name":"Copy Paste","description":"Issues related to copy paste","color":"b4f0a9"},"Drag & Drop":{"name":"Drag & Drop","description":"Issues related to the drag & drop experience","color":"92115a"},"BE Coders Pod":{"color":"5d9848","name":"BE Coders Pod","description":"Issues related to users writing code to fetch and update data"},"FE Coders Pod":{"color":"a7effc","name":"FE Coders Pod","description":"Issues related to users writing javascript in appsmith"},"New Developers Pod":{"color":"6310da","name":"New Developers Pod","description":"Issues that new developers face while exploring the IDE"},"Sniping Mode":{"name":"Sniping Mode","description":"Issues related to sniping mode","color":"30c76d"},"Redis":{"name":"Redis","description":"Issues related to Redis","color":"8078b0"},"New Datasource":{"color":"60b14c","name":"New Datasource","description":"Requests for new datasources"},"Evaluated Value":{"name":"Evaluated Value","description":"Issues related to evaluated values","color":"39f6e7"},"Undo/Redo":{"name":"Undo/Redo","description":"Issues related to undo/redo","color":"f25880"},"App Navigation":{"name":"App Navigation","description":"Issues related to the topbar navigation and configuring it","color":"12b715"},"Responsive Viewport":{"color":"d12d2e","name":"Responsive Viewport","description":"Issues seen on different viewports like mobile"},"Widgets Pane":{"name":"Widgets Pane","description":"Issues related to the discovery and organisation of widgets","color":"ad5d78"},"View Mode":{"color":"1799b0","name":"View Mode","description":"Issues related to the view mode"},"User Education Pod":{"name":"User Education Pod","description":"Issues related to user education","color":"1799b0"},"Content":{"name":"Content","description":"For content related topics i.e blogs, templates, videos","color":"a8dff7"},"Embedding Apps":{"name":"Embedding Apps","description":"Issues related to embedding","color":"30c76d"},"Slash Command":{"name":"Slash Command","description":"Issues related to the slash command","color":"a0608e"},"Widget Property":{"name":"Widget Property","description":"Issues related to adding / modifying widget properties across widgets","color":"5e92cb"},"Windows":{"name":"Windows","description":"Issues related exclusively to Windows systems","color":"b4cb8a"},"Old App Issues":{"name":"Old App Issues","description":"Issues related to apps old apps a few weeks old and app issues in stale browser session","color":"87ab18"},"Document Viewer Widget":{"name":"Document Viewer Widget","description":"Issues related to Document Viewer Widget","color":"899d4b"},"Radio Group Widget":{"name":"Radio Group Widget","description":"Issues related to radio group widget","color":"b68495"},"Super Admin":{"name":"Super Admin","description":"Issues related to the super admin page","color":"aa95cf"},"Postgres":{"name":"Postgres","description":"Postgres related issues","color":"8078b0"},"REST API plugin":{"name":"REST API plugin","description":"REST API plugin related issues","color":"8078b0"},"New JS Function":{"name":"New JS Function","description":"Issues related to adding a JS Function","color":"8e8aa4"},"Cannot Reproduce Issue":{"color":"93c9cc","name":"Cannot Reproduce Issue","description":"Issues that cannot be reproduced"},"Widget Grouping":{"name":"Widget Grouping","description":"Issues related to Widget Grouping","color":"a49951"},"K8s":{"name":"K8s","description":"Kubernetes related issues","color":"5f318a"},"Docker":{"name":"Docker","description":"Issues related to docker","color":"89b808"},"Camera Widget":{"name":"Camera Widget","description":"Issues and enhancements related to camera widget","color":"e6038e"},"SAAS Plugins":{"name":"SAAS Plugins","description":"Issues related to SAAS Plugins","color":"ef9c9d"},"JS Promises":{"name":"JS Promises","description":"Issues related to promises","color":"d7771f"},"OnPageLoad":{"name":"OnPageLoad","description":"OnPageLoad issues on functions and queries","color":"50559d"},"JS Usability":{"name":"JS Usability","description":"usability issues with JS editor and JS elsewhere","color":"a302b0"},"Currency Input Widget":{"name":"Currency Input Widget","description":"Issues related to currency input widget","color":"b2164f"},"TreeSelect":{"name":"TreeSelect","description":"Issues related to TreeSelect Widget","color":"a1633e"},"MultiTree Select Widget":{"name":"MultiTree Select Widget","description":"Issues related to MultiTree Select Widget","color":"a1633e"},"Welcome Screen":{"name":"Welcome Screen","description":"Issues related to the welcome screen","color":"30c76d"},"Realtime Commenting":{"color":"a70b86","name":"Realtime Commenting","description":"In-app communication between teams"},"Phone Input Widget":{"name":"Phone Input Widget","description":"Issues related to the Phone Input widget","color":"a70b86"},"JSON Form":{"name":"JSON Form","description":"Issue / features related to the JSON form wiget","color":"46b209"},"All Widgets":{"name":"All Widgets","description":"Issues related to all widgets","color":"972b36"},"V1":{"name":"V1","description":"V1","color":"67ab2e"},"Reflow & Resize":{"name":"Reflow & Resize","description":"All issues related to reflow and resize experience","color":"748a13"},"App Theming":{"name":"App Theming","description":"Items that are related to the App level theming controls epic","color":"905420"},"SSO":{"name":"SSO","description":"Issues, requests and enhancements around Single sign-on.","color":"bf019b"},"Multi User Realtime":{"name":"Multi User Realtime","description":"Issues related to multiple users using or editing an application","color":"e7b6ce"},"Templates":{"name":"Templates","description":"Issues related to templates","color":"b7e568"},"Ready for design":{"name":"Ready for design","description":"this issue is ready for design: it contains clear problem statements and other required information","color":"ebf442"},"Support":{"name":"Support","description":"Issues created by the A-force team to address user queries","color":"1740f3"},"Button Group widget":{"name":"Button Group widget","description":"Issue and enhancements related to the button group widget","color":"f17025"},"GraphQL Plugin":{"name":"GraphQL Plugin","description":"Issues related to GraphQL plugin","color":"8078b0"},"DevOps Pod":{"name":"DevOps Pod","description":"Issues related to devops","color":"d956c7"},"medium":{"name":"medium","description":"Issues that frustrate users due to poor UX","color":"23dfd9"},"ArangoDB":{"name":"ArangoDB","description":"Issues related to arangoDB","color":"8078b0"},"Code Refactoring":{"name":"Code Refactoring","description":"Issues related to code refactoring","color":"76310e"},"Progress bar widget":{"name":"Progress bar widget","description":"To track issues related to progress bar","color":"2d7abf"},"Audio Recorder Widget":{"name":"Audio Recorder Widget","description":"Issues related to Audio Recorder Widget","color":"9accef"},"Airtable":{"name":"Airtable","description":"Issues for Airtable","color":"60885f"},"RBAC":{"name":"RBAC","description":"Issues, requests and enhancements around RBAC.","color":"9211c3"},"Canvas / Grid":{"name":"Canvas / Grid","description":"Issues related to the canvas","color":"16b092"},"Email Config":{"name":"Email Config","description":"Issues related to configuring the email service","color":"2a21d1"},"CURL":{"name":"CURL","description":"Issues related to CURL impor","color":"60885f"},"Canvas Zooms":{"name":"Canvas Zooms","description":"Issues related to zooming the canvas","color":"e6038e"},"business":{"name":"business","description":"Features that will be a part of our business edition","color":"cd59eb"},"Action Pod":{"name":"Action Pod","description":"","color":"ee2e36"},"AutomationGap1":{"color":"a5e07c","name":"AutomationGap1","description":"Issues that needs automated tests"},"A-Force11":{"name":"A-Force11","description":"Issues raised by A-Force team","color":"d667b6"},"Business Edition":{"name":"Business Edition","description":"Features that will be a part of our business edition","color":"89bb6c"},"storeValue":{"name":"storeValue","description":"Issues related to the store value function","color":"5d3e66"},"Tests":{"name":"Tests","description":"test item","color":"1c6990"},"DynamoDB":{"name":"DynamoDB","description":"Issues that are related to DynamoDB should have this label","color":"60885f"},"Design System Pod":{"name":"Design System Pod","description":"Appsmith design system related issues","color":"706f03"},"ABAC":{"color":"e009a5","name":"ABAC","description":"User permissions and access controls"},"Backup & Restore":{"name":"Backup & Restore","description":"Issues related to backup and restore","color":"86874d"},"Billing":{"name":"Billing","description":"Billing infrastructure and flows for Business Edition and Trial users","color":"d2bc40"},"Datatype issue":{"name":"Datatype issue","description":"Issues that have risen because data types weren't handled","color":"60885f"},"OAuth":{"name":"OAuth","description":"OAuth related bugs or features","color":"60885f"},"Table Widget V2":{"name":"Table Widget V2","description":"Issues related to Table Widget V2","color":"3a7192"},"IDE Navigation":{"name":"IDE Navigation","description":"Issues/feature requests related to IDE navigation, and context switching","color":"bc0cba"},"Query performance":{"name":"Query performance","description":"Issues that have to do with lack in performance of query execution","color":"e4d966"},"SAAS Manager App":{"name":"SAAS Manager App","description":"Issues with the SAAS manager app","color":"d427db"},"Twilio":{"name":"Twilio","description":"Issues related to Twilio integration","color":"23ba8d"},"Hubspot":{"name":"Hubspot","description":"Issues related to Hubspot integration","color":"60885f"},"Zendesk":{"name":"Zendesk","description":"Issues related to Zendesk integration","color":"60885f"},"Entity Refactor":{"name":"Entity Refactor","description":"Issues related to refactor logic","color":"418fa4"},"Branding":{"name":"Branding","description":"All issues under branding and whitelabelling appsmith ecosystem","color":"7aaaf1"},"Map Chart Widget":{"name":"Map Chart Widget","description":"Issues related to Map Chart Widgets","color":"c8397f"},"Product Catchup":{"name":"Product Catchup","description":"Issues created in the product catchup","color":"29cd2c"},"Framework Functions":{"name":"Framework Functions","description":"Issues related to internal functions like showAlert(), navigateTo() etc...","color":"c25a09"},"Frontend Libraries Upgrade":{"name":"Frontend Libraries Upgrade","description":"Issues related to frontend libraries upgrade","color":"ede1fc"},"Audit Logs":{"name":"Audit Logs","description":"Audit trails to ensure data security","color":"f3fd62"},"MsSQL":{"name":"MsSQL","description":"Issues related to MsSQL plugin","color":"8078b0"},"Data Platform Pod":{"name":"Data Platform Pod","description":"Issues related to the underlying data platform","color":"3f8c3a"},"Integrations Pod":{"name":"Integrations Pod","description":"Issues related to a specific integration","color":"5dbbb1"},"Datasource Environments":{"name":"Datasource Environments","description":"Issues related to datasource environments","color":"bb7a14"},"Elastic Search":{"name":"Elastic Search","description":"Issues related to the elastic search datasource","color":"8078b0"},"Core Query Execution":{"color":"418fa4","name":"Core Query Execution","description":"Issues related to the execution of all queries"},"Query Management":{"name":"Query Management","description":"Issues related to the CRUD of actions or queries","color":"6a5b42"},"Query Settings":{"name":"Query Settings","description":"Issues related to the settings of all queries","color":"c7da7a"},"Code Editor":{"name":"Code Editor","description":"Issues related to the code editor","color":"4ca16e"},"Query Forms":{"color":"12b253","name":"Query Forms","description":"Isuses related to the query forms"},"JS Objects":{"color":"22962c","name":"JS Objects","description":"Issues related to JS Objects"},"JS Evaluation":{"color":"22962c","name":"JS Evaluation","description":"Issues related to JS evaluation on the platform"},"SmartSubstitution":{"name":"SmartSubstitution","description":"Issues related to Smart substitution of mustache bindings in queries","color":"e4d966"},"Query Generation":{"name":"Query Generation","description":"Issues related to query generation","color":"e4d966"},"Suggested Widgets":{"name":"Suggested Widgets","description":"Issues related to suggesting widgets based on query response","color":"e4d966"},"Page load executions":{"name":"Page load executions","description":"Issues related to page load execution","color":"5696b2"},"Code Scanner Widget":{"name":"Code Scanner Widget","description":"Issues related to code scanner widget","color":"9bc1a0"},"Clean URLs":{"name":"Clean URLs","description":"Issues related to clean URLs epic","color":"112623"},"Widget keyboard accessibility":{"name":"Widget keyboard accessibility","description":"All issues related to keyboard accessibility in widgets","color":"b626fd"},"Connection pool":{"name":"Connection pool","description":"issues to do with connection pooling of various plugins","color":"94fe36"},"List Widget V2":{"name":"List Widget V2","description":"Issues related to the list widget v2","color":"adaaf7"},"Auto Height":{"name":"Auto Height","description":"Issues related to dynamic height of widgets","color":"5149cf"},"cypress_failed_test":{"name":"cypress_failed_test","description":"Cypress failed tests","color":"4745d5"},"Needs validation":{"name":"Needs validation","description":"Needs problem validation before being picked up","color":"66673d"},"Slider Widget":{"name":"Slider Widget","description":"Issues raised for slider widgets.","color":"2eef5f"},"Multitenancy":{"name":"Multitenancy","description":"Support multitenancy within single appsmith instance","color":"8c49a9"},"Git Pod":{"name":"Git Pod","description":"Anything related to git sync","color":"2e5ba4"},"Mobile Pod":{"name":"Mobile Pod","description":"All issues related to mobile responsiveness","color":"6c97fd"},"Responsive Widget":{"name":"Responsive Widget","description":"All issues related to widget responsiveness","color":"d12d2e"},"Responsive Canvas":{"name":"Responsive Canvas","description":"All issues related to canvas responsiveness","color":"45a0a8"},"Conversion Algorithm":{"name":"Conversion Algorithm","description":"All issue related to converting app from fixed to flex mode & vice versa","color":"d12d2e"},"Spacing":{"name":"Spacing","description":"All issue related to spacing between widgets in auto layout","color":"d12d2e"},"Browser specific":{"name":"Browser specific","description":"All issue related to browser","color":"d12d2e"},"Error Handling":{"name":"Error Handling","description":"Issues related to error handling","color":"4e1872"},"Performance infra":{"name":"Performance infra","description":"all issue related to the performance infra","color":"8a60f6"},"DSL Update":{"name":"DSL Update","description":"Issues related to storing and updating the DSL","color":"e16cf3"},"AST-frontend":{"name":"AST-frontend","description":"Issues related to maintaining AST logic","color":"434a3a"},"AST-backend":{"name":"AST-backend","description":"Backend issues related to AST parsing","color":"c476eb"},"MariaDB":{"name":"MariaDB","description":"MariaDB datasource","color":"8428c3"},"Billing & Usage Pod":{"name":"Billing & Usage Pod","description":"Issues pertaining to licensing, billing, usage across self serve and enterprise customers","color":"256808"},"ADS Component Issue":{"name":"ADS Component Issue","description":"Issues which are caused due to ADS components","color":"d89119"},"Regressed":{"color":"723fd0","name":"Regressed","description":"Scenarios that were working before but have now regressed"},"Needs RCA":{"name":"Needs RCA","description":"a critical or high priority issue that needs an RCA","color":"2cc68f"},"Custom JS Libraries":{"name":"Custom JS Libraries","description":"Issues related to adding custom JS library","color":"bacb6d"},"Integrations Pod General":{"name":"Integrations Pod General","description":"Issues related to the Integrations Pod that don't fit into other tags.","color":"287823"},"Performance Pod":{"name":"Performance Pod","description":"All things related to Appsmith performance","color":"b5a25d"},"Performance":{"name":"Performance","description":"Issues related to performance","color":"9a18d7"},"File upload issues":{"name":"File upload issues","description":"Issues related to uploading any type of files from within Appsmith","color":"8154df"},"Action Selector":{"name":"Action Selector","description":"Issues related to action selector on the property pane","color":"2f9e20"},"Widget design system":{"name":"Widget design system","description":"","color":"cb6188"},"Deploy App":{"name":"Deploy App","description":"Issues related to app deployment","color":"6f6152"},"Community Reported":{"name":"Community Reported","description":"issues reported by community members","color":"1402e5"},"JS Function execution":{"name":"JS Function execution","description":"JS function execution","color":"7c2de1"},"Self Serve":{"name":"Self Serve","description":"For all issues related to self-serve flow for business edition","color":"4dacfc"},"Self Serve 1.0":{"name":"Self Serve 1.0","description":"For all issues related to v1 of the self serve project","color":"ae839e"},"CE Instance":{"name":"CE Instance","description":"For all issues relating to usage, licensing or billing on the CE instance","color":"d2bc40"},"Customer Portal":{"name":"Customer Portal","description":"For all tasks/issues pertaining to customer.appsmith.com","color":"d2bc40"},"Cloud Services":{"name":"Cloud Services","description":"For all tasks/issues on Appsmith cloud-services relating to licensing, usage and billing","color":"d2bc40"},"Billing Integrations":{"name":"Billing Integrations","description":"For all issues relating to 3P integrations Appsmith is using for billing & usage","color":"d2bc40"},"One-click Binding":{"name":"One-click Binding","description":"Issues related to the One click binding epic","color":"f1661c"},"Airgap":{"name":"Airgap","description":"Tickets related to supporting air-gapped Appsmith instances","color":"1cb294"},"SMTP plugin":{"name":"SMTP plugin","description":"Issues related to SMTP plugin","color":"541457"},"AWS AMI":{"name":"AWS AMI","description":"Issues Related to AWS AMI","color":"b44680"},"Old widget version":{"name":"Old widget version","description":"Use this label to raise issue specific only to an older version of a widget","color":"ff3814"},"Enterprise Billing":{"name":"Enterprise Billing","description":"To track all tasks/issues related to licensing & billing for enterprise customers","color":"14c156"},"Appsmith Business Cloud":{"name":"Appsmith Business Cloud","description":"Issues related to our business cloud offering","color":"89bb6c"},"Oracle SQL DB":{"name":"Oracle SQL DB","description":"Issues related to the Oracle plugin","color":"cbabcb"},"Community Contributor":{"name":"Community Contributor","description":"Meant to track issues that are assigned to external contributors","color":"149ab6"},"widget vertical alignment":{"name":"widget vertical alignment","description":"All issue related widget vertical alignment on the auto layout canvas","color":"d12d2e"},"Observability":{"name":"Observability","description":"Issues related to observability on the Appsmith instance","color":"dff913"},"Checkbox Component":{"name":"Checkbox Component","description":"This labels deals with checkbox component in wds package","color":"75a401"},"In-app ramps":{"name":"In-app ramps","description":"For all tasks/issues relating to adding in-app ramps in the community edition of the product","color":"8abae0"},"Analytics Improvements":{"name":"Analytics Improvements","description":"For all tasks focused on improving our overall analytics and fixing any issues ","color":"29b8ed"},"WDS team":{"name":"WDS team","description":"","color":"8d675a"},"Enterprise Edition":{"name":"Enterprise Edition","description":"Features that will be supported in Enterprise Edition only","color":"984f5e"},"Query filter":{"name":"Query filter","description":"Issues related to query filtering, e.g., WHERE clause","color":"a15134"},"Keyboard accessibility ":{"name":"Keyboard accessibility ","description":"All issue related to ADS component keyboard accessibility","color":"2ba696"},"Toggle button":{"name":"Toggle button","description":"All issue related to ADS toggle button","color":"edc47f"},"1-click upgrade":{"name":"1-click upgrade","description":"For all issues/tasks related to 1-click upgrade & downgrade project","color":"129082"},"Feature Flagging":{"name":"Feature Flagging","description":"Anything related feature flagging","color":"8d8a09"},"SCIM":{"name":"SCIM","description":"Label to collate our SCIM issues","color":"61a852"},"ADS Category Token":{"name":"ADS Category Token","description":"All issues related appsmith design system category tokens","color":"920961"},"ADS Component Documentation":{"name":"ADS Component Documentation","description":"All issues Appsmith design system component documentation","color":"64c46a"},"ADS Migration":{"name":"ADS Migration","description":"All issues related to Appsmith design system migration","color":"b082d6"},"ADS Deduplication ":{"name":"ADS Deduplication ","description":"Replacing component with ADS components","color":"b082d6"},"ADS Revamp":{"name":"ADS Revamp","description":"All issues related to ads revamp. ","color":"b082d6"},"ADS Deduplication":{"name":"ADS Deduplication","description":"Replacing component with ADS components","color":"b082d6"},"ADS Grayscale":{"name":"ADS Grayscale","description":"Support grayscale color changes","color":"b03577"},"ADS Unit Test":{"name":"ADS Unit Test","description":"All issue related ads unit cases ","color":"b082d6"},"ADS Components":{"name":"ADS Components","description":"All issues related ADS components","color":"b082d6"},"Widget Discoverability":{"name":"Widget Discoverability","description":"Issues related to Widget Discoverability","color":"7b55ce"},"Widget setter method":{"name":"Widget setter method","description":"Issues with widget property setters","color":"8dce87"},"License":{"name":"License","description":"For all issues/tasks related to licensing of appsmith-ee edition","color":"90ee98"},"Templates pod":{"name":"Templates pod","description":"Issues related to Templates","color":"b7e568"},"Community template":{"name":"Community template","description":"Label for development of community templates and its integration to platform","color":"8a0510"},"DocumentDB":{"name":"DocumentDB","description":"Issues related to support DocumentDB in Appsmith Data layer","color":"2c8b56"},"Multiple Environments":{"name":"Multiple Environments","description":"Issues or tasks related to multiple environments","color":"4e972b"},"Platformization":{"name":"Platformization","description":"Issues or tasks related to platformization of Appsmith codebase","color":"4e972b"},"Activation - datasources":{"name":"Activation - datasources","description":"issues related to activation projects","color":"7c7ace"},"Partial-import-export":{"name":"Partial-import-export","description":"Label for granular reusability.","color":"1e439c"},"AI":{"name":"AI","description":"All tasks related to AI","color":"75c4ce"},"Custom environments":{"name":"Custom environments","description":"Issues with creating or working with custom environments","color":"2137d6"},"ADS Typography":{"name":"ADS Typography","description":"All issue related typographical changes","color":"2dbe8d"},"Auto Layout":{"name":"Auto Layout","description":"Issues relates to auto layout","color":"92cf8c"},"Heroku":{"name":"Heroku","description":"Issues related to Heroku","color":"a81b69"},"ADS Visual Styles":{"name":"ADS Visual Styles","description":"All issues related to ADS visual styles","color":"d3da89"},"ADS Component Design":{"name":"ADS Component Design","description":"All issue related to component design","color":"5cc91e"},"Modal Component":{"name":"Modal Component","description":"All issue related to ads modal component","color":"ee63f3"},"App setting":{"name":"App setting","description":"Related to app settings panel within the app","color":"144206"},"BE instance":{"name":"BE instance","description":"For all issues related to license, billing on BE instance","color":"ae8f98"},"Schema":{"name":"Schema","description":"Issues related to database schema","color":"c470c2"},"Fixed layout":{"name":"Fixed layout","description":"issues related to fixed layout","color":"b66681"},"Anvil layout":{"name":"Anvil layout","description":"issues related to the new layout system anvil","color":"722bf0"},"New Deployment Mode":{"name":"New Deployment Mode","description":"Support a new mode of deployment","color":"108033"},"Custom widgets":{"name":"Custom widgets","description":"For all issues related to the custom widget project","color":"c9db9c"},"IDE Pod":{"name":"IDE Pod","description":"https://app.zenhub.com/workspaces/new-developers-pod-60507ad1d4b98d00150a2858/board","color":"d3d248"},"TM_BU":{"name":"TM_BU","description":"The issues on Team Manager which needs to be taken up by Billing & Usage","color":"198cdf"},"Homepage Experience V2":{"name":"Homepage Experience V2","description":"Label for reporting new tasks and bug fixes related to revamped homepage experience","color":"c55d54"},"Appsmith Labs":{"name":"Appsmith Labs","description":"All things related to AI and other new initiatives ","color":"712d51"},"Customer Success":{"name":"Customer Success","description":"Issues that the success team cares about","color":"6ccabd"},"Invite flow":{"name":"Invite flow","description":"Invite users flow and any associated actions","color":"881b35"},"Invite users":{"name":"Invite users","description":"Invite users flow and any associated actions","color":"23e6d6"},"Workflows Pod":{"name":"Workflows Pod","description":"For all issues related to the Workflows feature","color":"2c1f93"}},"success":true} \ No newline at end of file +{"runners":[{"versioning":{"source":"milestones","type":"SemVer"},"prereleaseName":"alpha","issue":{"labels":{"Error Handling":{"conditions":[],"requires":1},"Templates pod":{"conditions":[{"label":"Templates","type":"hasLabel","value":true},{"label":"Community template","type":"hasLabel","value":true},{"label":"Partial-import-export","type":"hasLabel","value":true}],"requires":1},"Team Managers Pod":{"conditions":[{"label":"Settings","type":"hasLabel","value":true},{"label":"Home Page","type":"hasLabel","value":true},{"label":"Realtime Commenting","type":"hasLabel","value":true},{"label":"SSO","type":"hasLabel","value":true},{"label":"Multi User Realtime","type":"hasLabel","value":true},{"label":"RBAC","type":"hasLabel","value":true},{"label":"ABAC","type":"hasLabel","value":true},{"label":"Audit Logs","type":"hasLabel","value":true},{"label":"Multitenancy","type":"hasLabel","value":true},{"label":"Airgap","type":"hasLabel","value":true},{"label":"Enterprise Edition","type":"hasLabel","value":true},{"label":"SCIM","type":"hasLabel","value":true},{"label":"Invite flow","type":"hasLabel","value":true}],"requires":1},"New Developers Pod":{"conditions":[{"label":"Omnibar","type":"hasLabel","value":true},{"label":"Telemetry","type":"hasLabel","value":true},{"label":"Entity Explorer","type":"hasLabel","value":true},{"label":"IDE","type":"hasLabel","value":true},{"label":"Example Apps","type":"hasLabel","value":true},{"label":"i18n","type":"hasLabel","value":true},{"label":"IDE Navigation","type":"hasLabel","value":true},{"label":"Clean URLs","type":"hasLabel","value":true},{"label":"In App Comms","type":"hasLabel","value":true},{"label":"In App Comms","type":"hasLabel","value":true},{"label":"App setting","type":"hasLabel","value":true}],"requires":1},"BE Coders Pod":{"conditions":[{"label":"SAAS Plugins","type":"hasLabel","value":true},{"label":"SAAS Manager App","type":"hasLabel","value":true},{"label":"Data Platform Pod","type":"hasLabel","value":true},{"label":"Integrations Pod","type":"hasLabel","value":true}],"requires":1},"FE Coders Pod":{"conditions":[{"label":"JS Linting & Errors","type":"hasLabel","value":true},{"label":"Debugger","type":"hasLabel","value":true},{"label":"JS Snippets","type":"hasLabel","value":true},{"label":"Autocomplete","type":"hasLabel","value":true},{"label":"Evaluated Value","type":"hasLabel","value":true},{"label":"Slash Command","type":"hasLabel","value":true},{"label":"New JS Function","type":"hasLabel","value":true},{"label":"JS Promises","type":"hasLabel","value":true},{"label":"JS Usability","type":"hasLabel","value":true},{"label":"Code Refactoring","type":"hasLabel","value":true},{"label":"storeValue","type":"hasLabel","value":true},{"label":"OnPageLoad","type":"hasLabel","value":true},{"label":"Framework Functions","type":"hasLabel","value":true},{"label":"Code Editor","type":"hasLabel","value":true},{"label":"JS Objects","type":"hasLabel","value":true},{"label":"JS Evaluation","type":"hasLabel","value":true},{"label":"AST-frontend","type":"hasLabel","value":true},{"label":"Custom JS Libraries","type":"hasLabel","value":true},{"label":"Action Selector","type":"hasLabel","value":true},{"label":"JS Function execution","type":"hasLabel","value":true},{"label":"Widget setter method","type":"hasLabel","value":true},{"label":"Error Handling","type":"hasLabel","value":true}],"requires":1},"App Viewers Pod":{"conditions":[{"label":"Button Widget","type":"hasLabel","value":true},{"label":"Chart Widget","type":"hasLabel","value":true},{"label":"Container Widget","type":"hasLabel","value":true},{"label":"Date Picker Widget","type":"hasLabel","value":true},{"label":"Select Widget","type":"hasLabel","value":true},{"label":"File Picker Widget","type":"hasLabel","value":true},{"label":"Form Widget","type":"hasLabel","value":true},{"label":"Image Widget","type":"hasLabel","value":true},{"label":"Input Widget","type":"hasLabel","value":true},{"label":"List Widget","type":"hasLabel","value":true},{"label":"MultiSelect Widget","type":"hasLabel","value":true},{"label":"Map Widget","type":"hasLabel","value":true},{"label":"Modal Widget","type":"hasLabel","value":true},{"label":"Radio Widget","type":"hasLabel","value":true},{"label":"Rich Text Editor Widget","type":"hasLabel","value":true},{"label":"Tab Widget","type":"hasLabel","value":true},{"label":"Table Widget","type":"hasLabel","value":true},{"label":"Text Widget","type":"hasLabel","value":true},{"label":"Video Widget","type":"hasLabel","value":true},{"label":"iFrame","type":"hasLabel","value":true},{"label":"Menu Button","type":"hasLabel","value":true},{"label":"Rating","type":"hasLabel","value":true},{"label":"Widget Validation","type":"hasLabel","value":true},{"label":"reallabel","type":"hasLabel","value":true},{"label":"New Widget","type":"hasLabel","value":true},{"label":"Switch widget","type":"hasLabel","value":true},{"label":"Audio Widget","type":"hasLabel","value":true},{"label":"Icon Button Widget","type":"hasLabel","value":true},{"label":"Stat Box Widget","type":"hasLabel","value":true},{"label":"Voice Recorder Widget","type":"hasLabel","value":true},{"label":"Calendar Widget","type":"hasLabel","value":true},{"label":"Menu Button Widget","type":"hasLabel","value":true},{"label":"Divider Widget","type":"hasLabel","value":true},{"label":"Rating Widget","type":"hasLabel","value":true},{"label":"App Navigation","type":"hasLabel","value":true},{"label":"View Mode","type":"hasLabel","value":true},{"label":"Widget Property","type":"hasLabel","value":true},{"label":"Document Viewer Widget","type":"hasLabel","value":true},{"label":"Radio Group Widget","type":"hasLabel","value":true},{"label":"Currency Input Widget","type":"hasLabel","value":true},{"label":"TreeSelect","type":"hasLabel","value":true},{"label":"MultiTree Select Widget","type":"hasLabel","value":true},{"label":"Phone Input Widget","type":"hasLabel","value":true},{"label":"JSON Form","type":"hasLabel","value":true},{"label":"All Widgets","type":"hasLabel","value":true},{"label":"Button Group widget","type":"hasLabel","value":true},{"label":"Progress bar widget","type":"hasLabel","value":true},{"label":"Audio Recorder Widget","type":"hasLabel","value":true},{"label":"Camera Widget","type":"hasLabel","value":true},{"label":"Table Widget V2","type":"hasLabel","value":true},{"label":"Branding","type":"hasLabel","value":true},{"label":"Map Chart Widget","type":"hasLabel","value":true},{"label":"Code Scanner Widget","type":"hasLabel","value":true},{"label":"Widget keyboard accessibility","type":"hasLabel","value":true},{"label":"List Widget V2","type":"hasLabel","value":true},{"label":"Slider Widget","type":"hasLabel","value":true},{"label":"One-click Binding","type":"hasLabel","value":true},{"label":"Old widget version","type":"hasLabel","value":true},{"label":"Widget Discoverability","type":"hasLabel","value":true},{"label":"Custom widgets","type":"hasLabel","value":true}],"requires":1},"UI Builders Pod":{"conditions":[{"label":"Property Pane","type":"hasLabel","value":true},{"label":"Pages","type":"hasLabel","value":true},{"label":"Copy Paste","type":"hasLabel","value":true},{"label":"Drag & Drop","type":"hasLabel","value":true},{"label":"Undo/Redo","type":"hasLabel","value":true},{"label":"Widgets Pane","type":"hasLabel","value":true},{"label":"UI Performance","type":"hasLabel","value":true},{"label":"Widget Grouping","type":"hasLabel","value":true},{"label":"Reflow & Resize","type":"hasLabel","value":true},{"label":"Canvas / Grid","type":"hasLabel","value":true},{"label":"Canvas Zooms","type":"hasLabel","value":true},{"label":"Frontend Libraries Upgrade","type":"hasLabel","value":true},{"label":"Auto Height","type":"hasLabel","value":true},{"label":"Responsive Canvas","type":"hasLabel","value":true},{"label":"Responsive Widget","type":"hasLabel","value":true},{"label":"Responsive Viewport","type":"hasLabel","value":true},{"label":"Conversion Algorithm","type":"hasLabel","value":true},{"label":"Spacing","type":"hasLabel","value":true},{"label":"Browser specific","type":"hasLabel","value":true},{"label":"widget vertical alignment","type":"hasLabel","value":true},{"label":"Auto Layout","type":"hasLabel","value":true},{"label":"Fixed layout","type":"hasLabel","value":true},{"label":"Anvil layout","type":"hasLabel","value":true}],"requires":1},"User Education Pod":{"conditions":[{"label":"Content","type":"hasLabel","value":true},{"label":"Documentation","type":"hasLabel","value":true}],"requires":1},"DevOps Pod":{"conditions":[{"label":"Docker","type":"hasLabel","value":true},{"label":"Super Admin","type":"hasLabel","value":true},{"label":"Deployment","type":"hasLabel","value":true},{"label":"K8s","type":"hasLabel","value":true},{"label":"Email Config","type":"hasLabel","value":true},{"label":"Backup & Restore","type":"hasLabel","value":true},{"label":"AWS AMI","type":"hasLabel","value":true},{"label":"Observability","type":"hasLabel","value":true},{"label":"Heroku","type":"hasLabel","value":true},{"label":"New Deployment Mode","type":"hasLabel","value":true}],"requires":1},"Design System Pod":{"conditions":[{"label":"Design System Pod","type":"hasLabel","value":true},{"label":"ADS Component Issue","type":"hasLabel","value":true},{"label":"Keyboard accessibility ","type":"hasLabel","value":true},{"label":"Toggle button","type":"hasLabel","value":true},{"label":"ADS Category Token","type":"hasLabel","value":true},{"label":"ADS Component Documentation","type":"hasLabel","value":true},{"label":"ADS Migration","type":"hasLabel","value":true},{"label":"ADS Deduplication ","type":"hasLabel","value":true},{"label":"ADS Revamp","type":"hasLabel","value":true},{"label":"ADS Deduplication","type":"hasLabel","value":true},{"label":"ADS Unit Test","type":"hasLabel","value":true},{"label":"ADS Components","type":"hasLabel","value":true},{"label":"ADS Grayscale","type":"hasLabel","value":true},{"label":"Design System","type":"hasLabel","value":true},{"label":"ADS Typography","type":"hasLabel","value":true},{"label":"ADS Visual Styles","type":"hasLabel","value":true},{"label":"ADS Component Design","type":"hasLabel","value":true},{"label":"Modal Component","type":"hasLabel","value":true}],"requires":1},"Data Platform Pod":{"conditions":[{"label":"Datasource Environments","type":"hasLabel","value":true},{"label":"Datatype issue","type":"hasLabel","value":true},{"label":"Entity Refactor","type":"hasLabel","value":true},{"label":"Core Query Execution","type":"hasLabel","value":true},{"label":"Query Management","type":"hasLabel","value":true},{"label":"Query Settings","type":"hasLabel","value":true},{"label":"SmartSubstitution","type":"hasLabel","value":true},{"label":"Query Generation","type":"hasLabel","value":true},{"label":"Query performance","type":"hasLabel","value":true},{"label":"Suggested Widgets","type":"hasLabel","value":true},{"label":"Page load executions","type":"hasLabel","value":true},{"label":"DSL Update","type":"hasLabel","value":true},{"label":"AST-backend","type":"hasLabel","value":true},{"label":"Deploy App","type":"hasLabel","value":true},{"label":"File upload issues","type":"hasLabel","value":true},{"label":"Datasources","type":"hasLabel","value":true},{"label":"DocumentDB","type":"hasLabel","value":true},{"label":"Multiple Environments","type":"hasLabel","value":true},{"label":"Platformization","type":"hasLabel","value":true},{"label":"Custom environments","type":"hasLabel","value":true},{"label":"Schema","type":"hasLabel","value":true}],"requires":1},"Integrations Pod":{"conditions":[{"label":"New Datasource","type":"hasLabel","value":true},{"label":"Firestore","type":"hasLabel","value":true},{"label":"Google Sheets","type":"hasLabel","value":true},{"label":"Mongo","type":"hasLabel","value":true},{"label":"Redshift","type":"hasLabel","value":true},{"label":"snowflake","type":"hasLabel","value":true},{"label":"S3","type":"hasLabel","value":true},{"label":"Redis","type":"hasLabel","value":true},{"label":"Postgres","type":"hasLabel","value":true},{"label":"GraphQL Plugin","type":"hasLabel","value":true},{"label":"ArangoDB","type":"hasLabel","value":true},{"label":"MsSQL","type":"hasLabel","value":true},{"label":"REST API plugin","type":"hasLabel","value":true},{"label":"Elastic Search","type":"hasLabel","value":true},{"label":"OAuth","type":"hasLabel","value":true},{"label":"Airtable","type":"hasLabel","value":true},{"label":"CURL","type":"hasLabel","value":true},{"label":"DynamoDB","type":"hasLabel","value":true},{"label":"Zendesk","type":"hasLabel","value":true},{"label":"Hubspot","type":"hasLabel","value":true},{"label":"Query Forms","type":"hasLabel","value":true},{"label":"Twilio","type":"hasLabel","value":true},{"label":"MySQL","type":"hasLabel","value":true},{"label":"Connection pool","type":"hasLabel","value":true},{"label":"MariaDB","type":"hasLabel","value":true},{"label":"Integrations Pod General","type":"hasLabel","value":true},{"label":"SMTP plugin","type":"hasLabel","value":true},{"label":"Oracle SQL DB","type":"hasLabel","value":true},{"label":"Query filter","type":"hasLabel","value":true},{"label":"Activation - datasources","type":"hasLabel","value":true},{"label":"Onboarding","type":"hasLabel","value":true},{"label":"Generate Page","type":"hasLabel","value":true},{"label":"Sniping Mode","type":"hasLabel","value":true},{"label":"Welcome Screen","type":"hasLabel","value":true},{"label":"Login / Signup","type":"hasLabel","value":true}],"requires":1},"Git Pod":{"conditions":[{"label":"Git Version Control","type":"hasLabel","value":true},{"label":"Import-Export-App","type":"hasLabel","value":true},{"label":"Fork App","type":"hasLabel","value":true}],"requires":1},"Mobile Pod":{"conditions":[],"requires":1},"Billing & Usage Pod":{"conditions":[{"label":"CE Instance","type":"hasLabel","value":true},{"label":"Customer Portal","type":"hasLabel","value":true},{"label":"Cloud Services","type":"hasLabel","value":true},{"label":"Billing Integrations","type":"hasLabel","value":true},{"label":"Billing","type":"hasLabel","value":true},{"label":"Self Serve","type":"hasLabel","value":true},{"label":"Enterprise Billing","type":"hasLabel","value":true},{"label":"In-app ramps","type":"hasLabel","value":true},{"label":"Analytics Improvements","type":"hasLabel","value":true},{"label":"Self Serve 1.0","type":"hasLabel","value":true},{"label":"License","type":"hasLabel","value":true},{"label":"Appsmith Business Cloud","type":"hasLabel","value":true},{"label":"BE instance","type":"hasLabel","value":true},{"label":"Embedding Apps","type":"hasLabel","value":true},{"label":"TM_BU","type":"hasLabel","value":true},{"label":"Homepage Experience V2","type":"hasLabel","value":true},{"label":"Feature Flagging","type":"hasLabel","value":true},{"label":"Invite flow","type":"hasLabel","value":true},{"label":"Invite users","type":"hasLabel","value":true}],"requires":1},"Performance Pod":{"conditions":[{"label":"Performance","type":"hasLabel","value":true},{"label":"Performance infra","type":"hasLabel","value":true}],"requires":1},"Widget design system":{"conditions":[{"label":"App Theming","type":"hasLabel","value":true},{"label":"Widget Styling","type":"hasLabel","value":true},{"label":"Checkbox Group widget","type":"hasLabel","value":true},{"label":"Checkbox Widget","type":"hasLabel","value":true},{"label":"Checkbox Component","type":"hasLabel","value":true},{"label":"WDS team","type":"hasLabel","value":true},{"label":"Widget design system","type":"hasLabel","value":true}],"requires":1},"IDE Pod":{"conditions":[],"requires":1},"Appsmith Labs":{"conditions":[{"label":"AI","type":"hasLabel","value":true}],"requires":1},"Workflows Pod":{"conditions":[],"requires":1}}},"root":"."}],"labels":{"Tab Widget":{"color":"e2c76c","name":"Tab Widget","description":""},"Dont merge":{"color":"ADB39C","name":"Dont merge","description":""},"Epic":{"color":"3E4B9E","name":"Epic","description":"A zenhub epic that describes a project"},"Menu Button Widget":{"color":"235708","name":"Menu Button Widget","description":"Issues related to Menu Button widget"},"Checkbox Group widget":{"color":"88054d","name":"Checkbox Group widget","description":"Issues related to Checkbox Group Widget"},"Input Widget":{"color":"ae65d8","name":"Input Widget","description":""},"Security":{"color":"99139C","name":"Security","description":""},"QA":{"color":"e2ca68","name":"QA","description":""},"Verified":{"color":"9bf416","name":"Verified","description":""},"Wont Fix":{"color":"ffffff","name":"Wont Fix","description":"This will not be worked on"},"MySQL":{"color":"c9ddc6","name":"MySQL","description":"Issues related to MySQL plugin"},"Development":{"color":"9F8A02","name":"Development","description":""},"Help Wanted":{"color":"008672","name":"Help Wanted","description":"Extra attention is needed"},"Home Page":{"color":"9c0c8e","name":"Home Page","description":"Issues related to the application home page"},"Rating Widget":{"color":"235708","name":"Rating Widget","description":"Issues related to the rating widget"},"Stat Box Widget":{"color":"f1c9ce","name":"Stat Box Widget","description":"Issues related to stat box"},"Enhancement":{"color":"a2eeef","name":"Enhancement","description":"New feature or request"},"Settings":{"color":"f7ff60","name":"Settings","description":"organization, team & user settings"},"Fork App":{"color":"30c76d","name":"Fork App","description":"Issues related to forking apps"},"Container Widget":{"color":"19AD0D","name":"Container Widget","description":"Container widget"},"Papercut":{"color":"B562F6","name":"Papercut","description":""},"Needs Design":{"color":"bfd4f2","name":"Needs Design","description":"needs design or changes to design"},"i18n":{"color":"1799b0","name":"i18n","description":"Represents issues that need to be tackled to handle internationalization"},"Rich Text Editor Widget":{"color":"f72cac","name":"Rich Text Editor Widget","description":""},"Onboarding":{"color":"30c76d","name":"Onboarding","description":"Issues related to onboarding new developers"},"Pages":{"color":"d7fd80","name":"Pages","description":"Issues related to configuring pages"},"skip-changelog":{"color":"06086F","name":"skip-changelog","description":"Adding this label to a PR prevents it from being listed in the changelog"},"Low":{"color":"79e53b","name":"Low","description":"An issue that is neither critical nor breaks a user flow"},"potential-duplicate":{"color":"d3cb2e","name":"potential-duplicate","description":"This label marks issues that are potential duplicates of already open issues"},"Audio Widget":{"color":"447B9A","name":"Audio Widget","description":"Issues related to Audio Widget"},"Firestore":{"color":"8078b0","name":"Firestore","description":"Issues related to the firestore Integration"},"New Widget":{"color":"be4cf2","name":"New Widget","description":"A request for a new widget"},"Modal Widget":{"color":"03846f","name":"Modal Widget","description":""},"UX Improvement":{"color":"f4a089","name":"UX Improvement","description":""},"S3":{"color":"8078b0","name":"S3","description":"Issues related to the S3 plugin"},"Release Blocker":{"color":"5756bf","name":"Release Blocker","description":"This issue must be resolved before the release"},"safari":{"color":"51C6AA","name":"safari","description":"Bugs seen on safari browser"},"Example Apps":{"color":"1799b0","name":"Example Apps","description":"Example apps created for new signups"},"MultiSelect Widget":{"color":"AB62D4","name":"MultiSelect Widget","description":"Issues related to MultiSelect Widget"},"Widget Styling":{"color":"905420","name":"Widget Styling","description":"all about widget styling"},"Calendar Widget":{"color":"8c6644","name":"Calendar Widget","description":""},"Website":{"color":"151720","name":"Website","description":"Related to www.appsmith.com website"},"Low effort":{"color":"8B59F0","name":"Low effort","description":"Something that'll take a few days to build"},"App Viewers Pod":{"color":"cd8ef9","name":"App Viewers Pod","description":"This label assigns issues to the app viewers pod"},"Checkbox Widget":{"color":"88054d","name":"Checkbox Widget","description":""},"Spam":{"color":"620faf","name":"Spam","description":""},"Voice Recorder Widget":{"color":"85bc87","name":"Voice Recorder Widget","description":""},"Select Widget":{"color":"0c669e","name":"Select Widget","description":"Select or dropdown widget"},"Bug":{"color":"d73a4a","name":"Bug","description":"Something isn't working"},"Widget Validation":{"color":"6990BC","name":"Widget Validation","description":"Issues related to widget property validation"},"Generate Page":{"color":"30c76d","name":"Generate Page","description":"Issures related to page generation"},"File Picker Widget":{"color":"6ae4f2","name":"File Picker Widget","description":""},"snowflake":{"color":"8078b0","name":"snowflake","description":"Issues related to the snowflake Integration"},"Automation":{"color":"CCAF60","name":"Automation","description":""},"hotfix":{"color":"BA3F1D","name":"hotfix","description":""},"Team Managers Pod":{"color":"bddb81","name":"Team Managers Pod","description":"Issues that team managers care about for the security and efficiency of their teams"},"Import-Export-App":{"color":"15076d","name":"Import-Export-App","description":"Issues related to importing and exporting apps"},"High effort":{"color":"A7E87B","name":"High effort","description":"Something that'll take more than a month to build"},"Telemetry":{"color":"bc70f9","name":"Telemetry","description":"Issues related to instrumenting appsmith"},"Radio Widget":{"color":"91ef15","name":"Radio Widget","description":""},"Omnibar":{"color":"10b5ce","name":"Omnibar","description":"Issues related to the omnibar for navigation"},"Button Widget":{"color":"34efae","name":"Button Widget","description":""},"Switch widget":{"color":"33A8CE","name":"Switch widget","description":"The switch widget"},"Map Widget":{"color":"7eef7a","name":"Map Widget","description":""},"Task":{"color":"085630","name":"Task","description":"A simple Todo"},"Design System":{"color":"2958a4","name":"Design System","description":"Design system"},"opera":{"color":"C63F5B","name":"opera","description":"Any issues identified on the opera browser"},"Login / Signup":{"color":"30c76d","name":"Login / Signup","description":"Authentication flows"},"Image Widget":{"color":"8de8ad","name":"Image Widget","description":""},"firefox":{"color":"6d56e2","name":"firefox","description":""},"Property Pane":{"color":"b356ff","name":"Property Pane","description":"Issues related to the behaviour of the property pane"},"Deployment":{"color":"93491f","name":"Deployment","description":"Installation process of appsmith"},"Critical":{"color":"9b1b28","name":"Critical","description":"This issue needs immediate attention. Drop everything else"},"IDE":{"color":"61b2ee","name":"IDE","description":"Issues related to the IDE"},"Production":{"color":"b60205","name":"Production","description":""},"Dependencies":{"color":"0366d6","name":"Dependencies","description":"Pull requests that update a dependency file"},"Google Sheets":{"color":"8078b0","name":"Google Sheets","description":"Issues related to Google Sheets"},"Icon Button Widget":{"color":"D319CE","name":"Icon Button Widget","description":"Issues related to the icon button widget"},"Mongo":{"color":"8078b0","name":"Mongo","description":"Issues related to Mongo DB plugin"},"Documentation":{"color":"a8dff7","name":"Documentation","description":"Improvements or additions to documentation"},"TestGap":{"color":"f28253","name":"TestGap","description":"Issues identified for test plan improvement"},"keyboard shortcut":{"color":"0688B6","name":"keyboard shortcut","description":""},"Git Version Control":{"color":"858172","name":"Git Version Control","description":"Issues related to version control"},"Reopen":{"color":"897548","name":"Reopen","description":""},"Redshift":{"color":"8078b0","name":"Redshift","description":"Issues related to the redshift integration"},"Date Picker Widget":{"color":"ef1ce1","name":"Date Picker Widget","description":""},"Entity Explorer":{"color":"a2e2f9","name":"Entity Explorer","description":"Issues related to navigation using the entity explorer"},"JS Linting & Errors":{"color":"E56AA5","name":"JS Linting & Errors","description":"Issues related to JS Linting and errors"},"iFrame":{"color":"3CD1DB","name":"iFrame","description":"Issues related to iFrame"},"Stale":{"color":"ededed","name":"Stale","description":null},"Debugger":{"color":"e79062","name":"Debugger","description":"Issues related to the debugger"},"Quick effort":{"color":"95ED65","name":"Quick effort","description":"Something that'll take a few hours to build"},"Text Widget":{"color":"d130d1","name":"Text Widget","description":""},"Video Widget":{"color":"23dd4b","name":"Video Widget","description":""},"Datasources":{"color":"5052f6","name":"Datasources","description":"Issues related to configuring datasource on appsmith"},"error":{"color":"B66773","name":"error","description":"All issues connected to error messages"},"Form Widget":{"color":"09ed77","name":"Form Widget","description":""},"Needs Triaging":{"color":"e8b851","name":"Needs Triaging","description":"Needs attention from maintainers to triage"},"Autocomplete":{"color":"235708","name":"Autocomplete","description":"Issues related to the autocomplete"},"hacktoberfest":{"color":"0052cc","name":"hacktoberfest","description":"All issues that can be solved by the community during Hacktoberfest"},"Medium effort":{"color":"D31156","name":"Medium effort","description":"Something that'll take more than a week but less than a month to build"},"Release":{"color":"57e5e0","name":"Release","description":""},"High":{"color":"c94d14","name":"High","description":"This issue blocks a user from building or impacts a lot of users"},"UI Performance":{"color":"1799b0","name":"UI Performance","description":"Issues related to UI performance"},"UI Builders Pod":{"color":"517fba","name":"UI Builders Pod","description":"Issues that UI Builders face using appsmith"},"Deploy Preview":{"color":"bfdadc","name":"Deploy Preview","description":"Issues found in Deploy Preview"},"Needs Tests":{"color":"8ee263","name":"Needs Tests","description":"Needs automated tests to assert a feature/bug fix"},"Refactor":{"color":"B96662","name":"Refactor","description":"needs refactoring of code"},"Divider Widget":{"color":"235708","name":"Divider Widget","description":"Issues related to the divider widget"},"Table Widget":{"color":"2eead1","name":"Table Widget","description":""},"Needs More Info":{"color":"e54c10","name":"Needs More Info","description":"Needs additional information"},"Good First Issue":{"color":"7057ff","name":"Good First Issue","description":"Good for newcomers"},"UI Improvement":{"color":"9aeef4","name":"UI Improvement","description":""},"Backend":{"color":"d4c5f9","name":"Backend","description":"This marks the issue or pull request to reference server code"},"Frontend":{"color":"87c7f2","name":"Frontend","description":"This label marks the issue or pull request to reference client code"},"In App Comms":{"name":"In App Comms","description":"Issues around communication with appsmith instances","color":"463cca"},"Chart Widget":{"color":"616ecc","name":"Chart Widget","description":""},"List Widget":{"color":"8508A0","name":"List Widget","description":"Issues related to the list widget"},"Duplicate":{"color":"cfd3d7","name":"Duplicate","description":"This issue or pull request already exists"},"JS Snippets":{"color":"8d62d2","name":"JS Snippets","description":"issues related to JS Snippets"},"Copy Paste":{"name":"Copy Paste","description":"Issues related to copy paste","color":"b4f0a9"},"Drag & Drop":{"name":"Drag & Drop","description":"Issues related to the drag & drop experience","color":"92115a"},"BE Coders Pod":{"color":"5d9848","name":"BE Coders Pod","description":"Issues related to users writing code to fetch and update data"},"FE Coders Pod":{"color":"a7effc","name":"FE Coders Pod","description":"Issues related to users writing javascript in appsmith"},"New Developers Pod":{"color":"6310da","name":"New Developers Pod","description":"Issues that new developers face while exploring the IDE"},"Sniping Mode":{"name":"Sniping Mode","description":"Issues related to sniping mode","color":"30c76d"},"Redis":{"name":"Redis","description":"Issues related to Redis","color":"8078b0"},"New Datasource":{"color":"60b14c","name":"New Datasource","description":"Requests for new datasources"},"Evaluated Value":{"name":"Evaluated Value","description":"Issues related to evaluated values","color":"39f6e7"},"Undo/Redo":{"name":"Undo/Redo","description":"Issues related to undo/redo","color":"f25880"},"App Navigation":{"name":"App Navigation","description":"Issues related to the topbar navigation and configuring it","color":"12b715"},"Responsive Viewport":{"color":"d12d2e","name":"Responsive Viewport","description":"Issues seen on different viewports like mobile"},"Widgets Pane":{"name":"Widgets Pane","description":"Issues related to the discovery and organisation of widgets","color":"ad5d78"},"View Mode":{"color":"1799b0","name":"View Mode","description":"Issues related to the view mode"},"User Education Pod":{"name":"User Education Pod","description":"Issues related to user education","color":"1799b0"},"Content":{"name":"Content","description":"For content related topics i.e blogs, templates, videos","color":"a8dff7"},"Embedding Apps":{"name":"Embedding Apps","description":"Issues related to embedding","color":"30c76d"},"Slash Command":{"name":"Slash Command","description":"Issues related to the slash command","color":"a0608e"},"Widget Property":{"name":"Widget Property","description":"Issues related to adding / modifying widget properties across widgets","color":"5e92cb"},"Windows":{"name":"Windows","description":"Issues related exclusively to Windows systems","color":"b4cb8a"},"Old App Issues":{"name":"Old App Issues","description":"Issues related to apps old apps a few weeks old and app issues in stale browser session","color":"87ab18"},"Document Viewer Widget":{"name":"Document Viewer Widget","description":"Issues related to Document Viewer Widget","color":"899d4b"},"Radio Group Widget":{"name":"Radio Group Widget","description":"Issues related to radio group widget","color":"b68495"},"Super Admin":{"name":"Super Admin","description":"Issues related to the super admin page","color":"aa95cf"},"Postgres":{"name":"Postgres","description":"Postgres related issues","color":"8078b0"},"REST API plugin":{"name":"REST API plugin","description":"REST API plugin related issues","color":"8078b0"},"New JS Function":{"name":"New JS Function","description":"Issues related to adding a JS Function","color":"8e8aa4"},"Cannot Reproduce Issue":{"color":"93c9cc","name":"Cannot Reproduce Issue","description":"Issues that cannot be reproduced"},"Widget Grouping":{"name":"Widget Grouping","description":"Issues related to Widget Grouping","color":"a49951"},"K8s":{"name":"K8s","description":"Kubernetes related issues","color":"5f318a"},"Docker":{"name":"Docker","description":"Issues related to docker","color":"89b808"},"Camera Widget":{"name":"Camera Widget","description":"Issues and enhancements related to camera widget","color":"e6038e"},"SAAS Plugins":{"name":"SAAS Plugins","description":"Issues related to SAAS Plugins","color":"ef9c9d"},"JS Promises":{"name":"JS Promises","description":"Issues related to promises","color":"d7771f"},"OnPageLoad":{"name":"OnPageLoad","description":"OnPageLoad issues on functions and queries","color":"50559d"},"JS Usability":{"name":"JS Usability","description":"usability issues with JS editor and JS elsewhere","color":"a302b0"},"Currency Input Widget":{"name":"Currency Input Widget","description":"Issues related to currency input widget","color":"b2164f"},"TreeSelect":{"name":"TreeSelect","description":"Issues related to TreeSelect Widget","color":"a1633e"},"MultiTree Select Widget":{"name":"MultiTree Select Widget","description":"Issues related to MultiTree Select Widget","color":"a1633e"},"Welcome Screen":{"name":"Welcome Screen","description":"Issues related to the welcome screen","color":"30c76d"},"Realtime Commenting":{"color":"a70b86","name":"Realtime Commenting","description":"In-app communication between teams"},"Phone Input Widget":{"name":"Phone Input Widget","description":"Issues related to the Phone Input widget","color":"a70b86"},"JSON Form":{"name":"JSON Form","description":"Issue / features related to the JSON form wiget","color":"46b209"},"All Widgets":{"name":"All Widgets","description":"Issues related to all widgets","color":"972b36"},"V1":{"name":"V1","description":"V1","color":"67ab2e"},"Reflow & Resize":{"name":"Reflow & Resize","description":"All issues related to reflow and resize experience","color":"748a13"},"App Theming":{"name":"App Theming","description":"Items that are related to the App level theming controls epic","color":"905420"},"SSO":{"name":"SSO","description":"Issues, requests and enhancements around Single sign-on.","color":"bf019b"},"Multi User Realtime":{"name":"Multi User Realtime","description":"Issues related to multiple users using or editing an application","color":"e7b6ce"},"Templates":{"name":"Templates","description":"Issues related to templates","color":"b7e568"},"Ready for design":{"name":"Ready for design","description":"this issue is ready for design: it contains clear problem statements and other required information","color":"ebf442"},"Support":{"name":"Support","description":"Issues created by the A-force team to address user queries","color":"1740f3"},"Button Group widget":{"name":"Button Group widget","description":"Issue and enhancements related to the button group widget","color":"f17025"},"GraphQL Plugin":{"name":"GraphQL Plugin","description":"Issues related to GraphQL plugin","color":"8078b0"},"DevOps Pod":{"name":"DevOps Pod","description":"Issues related to devops","color":"d956c7"},"medium":{"name":"medium","description":"Issues that frustrate users due to poor UX","color":"23dfd9"},"ArangoDB":{"name":"ArangoDB","description":"Issues related to arangoDB","color":"8078b0"},"Code Refactoring":{"name":"Code Refactoring","description":"Issues related to code refactoring","color":"76310e"},"Progress bar widget":{"name":"Progress bar widget","description":"To track issues related to progress bar","color":"2d7abf"},"Audio Recorder Widget":{"name":"Audio Recorder Widget","description":"Issues related to Audio Recorder Widget","color":"9accef"},"Airtable":{"name":"Airtable","description":"Issues for Airtable","color":"60885f"},"RBAC":{"name":"RBAC","description":"Issues, requests and enhancements around RBAC.","color":"9211c3"},"Canvas / Grid":{"name":"Canvas / Grid","description":"Issues related to the canvas","color":"16b092"},"Email Config":{"name":"Email Config","description":"Issues related to configuring the email service","color":"2a21d1"},"CURL":{"name":"CURL","description":"Issues related to CURL impor","color":"60885f"},"Canvas Zooms":{"name":"Canvas Zooms","description":"Issues related to zooming the canvas","color":"e6038e"},"business":{"name":"business","description":"Features that will be a part of our business edition","color":"cd59eb"},"Action Pod":{"name":"Action Pod","description":"","color":"ee2e36"},"AutomationGap1":{"color":"a5e07c","name":"AutomationGap1","description":"Issues that needs automated tests"},"A-Force11":{"name":"A-Force11","description":"Issues raised by A-Force team","color":"d667b6"},"Business Edition":{"name":"Business Edition","description":"Features that will be a part of our business edition","color":"89bb6c"},"storeValue":{"name":"storeValue","description":"Issues related to the store value function","color":"5d3e66"},"Tests":{"name":"Tests","description":"test item","color":"1c6990"},"DynamoDB":{"name":"DynamoDB","description":"Issues that are related to DynamoDB should have this label","color":"60885f"},"Design System Pod":{"name":"Design System Pod","description":"Appsmith design system related issues","color":"706f03"},"ABAC":{"color":"e009a5","name":"ABAC","description":"User permissions and access controls"},"Backup & Restore":{"name":"Backup & Restore","description":"Issues related to backup and restore","color":"86874d"},"Billing":{"name":"Billing","description":"Billing infrastructure and flows for Business Edition and Trial users","color":"d2bc40"},"Datatype issue":{"name":"Datatype issue","description":"Issues that have risen because data types weren't handled","color":"60885f"},"OAuth":{"name":"OAuth","description":"OAuth related bugs or features","color":"60885f"},"Table Widget V2":{"name":"Table Widget V2","description":"Issues related to Table Widget V2","color":"3a7192"},"IDE Navigation":{"name":"IDE Navigation","description":"Issues/feature requests related to IDE navigation, and context switching","color":"bc0cba"},"Query performance":{"name":"Query performance","description":"Issues that have to do with lack in performance of query execution","color":"e4d966"},"SAAS Manager App":{"name":"SAAS Manager App","description":"Issues with the SAAS manager app","color":"d427db"},"Twilio":{"name":"Twilio","description":"Issues related to Twilio integration","color":"23ba8d"},"Hubspot":{"name":"Hubspot","description":"Issues related to Hubspot integration","color":"60885f"},"Zendesk":{"name":"Zendesk","description":"Issues related to Zendesk integration","color":"60885f"},"Entity Refactor":{"name":"Entity Refactor","description":"Issues related to refactor logic","color":"418fa4"},"Branding":{"name":"Branding","description":"All issues under branding and whitelabelling appsmith ecosystem","color":"7aaaf1"},"Map Chart Widget":{"name":"Map Chart Widget","description":"Issues related to Map Chart Widgets","color":"c8397f"},"Product Catchup":{"name":"Product Catchup","description":"Issues created in the product catchup","color":"29cd2c"},"Framework Functions":{"name":"Framework Functions","description":"Issues related to internal functions like showAlert(), navigateTo() etc...","color":"c25a09"},"Frontend Libraries Upgrade":{"name":"Frontend Libraries Upgrade","description":"Issues related to frontend libraries upgrade","color":"ede1fc"},"Audit Logs":{"name":"Audit Logs","description":"Audit trails to ensure data security","color":"f3fd62"},"MsSQL":{"name":"MsSQL","description":"Issues related to MsSQL plugin","color":"8078b0"},"Data Platform Pod":{"name":"Data Platform Pod","description":"Issues related to the underlying data platform","color":"3f8c3a"},"Integrations Pod":{"name":"Integrations Pod","description":"Issues related to a specific integration","color":"5dbbb1"},"Datasource Environments":{"name":"Datasource Environments","description":"Issues related to datasource environments","color":"bb7a14"},"Elastic Search":{"name":"Elastic Search","description":"Issues related to the elastic search datasource","color":"8078b0"},"Core Query Execution":{"color":"418fa4","name":"Core Query Execution","description":"Issues related to the execution of all queries"},"Query Management":{"name":"Query Management","description":"Issues related to the CRUD of actions or queries","color":"6a5b42"},"Query Settings":{"name":"Query Settings","description":"Issues related to the settings of all queries","color":"c7da7a"},"Code Editor":{"name":"Code Editor","description":"Issues related to the code editor","color":"4ca16e"},"Query Forms":{"color":"12b253","name":"Query Forms","description":"Isuses related to the query forms"},"JS Objects":{"color":"22962c","name":"JS Objects","description":"Issues related to JS Objects"},"JS Evaluation":{"color":"22962c","name":"JS Evaluation","description":"Issues related to JS evaluation on the platform"},"SmartSubstitution":{"name":"SmartSubstitution","description":"Issues related to Smart substitution of mustache bindings in queries","color":"e4d966"},"Query Generation":{"name":"Query Generation","description":"Issues related to query generation","color":"e4d966"},"Suggested Widgets":{"name":"Suggested Widgets","description":"Issues related to suggesting widgets based on query response","color":"e4d966"},"Page load executions":{"name":"Page load executions","description":"Issues related to page load execution","color":"5696b2"},"Code Scanner Widget":{"name":"Code Scanner Widget","description":"Issues related to code scanner widget","color":"9bc1a0"},"Clean URLs":{"name":"Clean URLs","description":"Issues related to clean URLs epic","color":"112623"},"Widget keyboard accessibility":{"name":"Widget keyboard accessibility","description":"All issues related to keyboard accessibility in widgets","color":"b626fd"},"Connection pool":{"name":"Connection pool","description":"issues to do with connection pooling of various plugins","color":"94fe36"},"List Widget V2":{"name":"List Widget V2","description":"Issues related to the list widget v2","color":"adaaf7"},"Auto Height":{"name":"Auto Height","description":"Issues related to dynamic height of widgets","color":"5149cf"},"cypress_failed_test":{"name":"cypress_failed_test","description":"Cypress failed tests","color":"4745d5"},"Needs validation":{"name":"Needs validation","description":"Needs problem validation before being picked up","color":"66673d"},"Slider Widget":{"name":"Slider Widget","description":"Issues raised for slider widgets.","color":"2eef5f"},"Multitenancy":{"name":"Multitenancy","description":"Support multitenancy within single appsmith instance","color":"8c49a9"},"Git Pod":{"name":"Git Pod","description":"Anything related to git sync","color":"2e5ba4"},"Mobile Pod":{"name":"Mobile Pod","description":"All issues related to mobile responsiveness","color":"6c97fd"},"Responsive Widget":{"name":"Responsive Widget","description":"All issues related to widget responsiveness","color":"d12d2e"},"Responsive Canvas":{"name":"Responsive Canvas","description":"All issues related to canvas responsiveness","color":"45a0a8"},"Conversion Algorithm":{"name":"Conversion Algorithm","description":"All issue related to converting app from fixed to flex mode & vice versa","color":"d12d2e"},"Spacing":{"name":"Spacing","description":"All issue related to spacing between widgets in auto layout","color":"d12d2e"},"Browser specific":{"name":"Browser specific","description":"All issue related to browser","color":"d12d2e"},"Error Handling":{"name":"Error Handling","description":"Issues related to error handling","color":"4e1872"},"Performance infra":{"name":"Performance infra","description":"all issue related to the performance infra","color":"8a60f6"},"DSL Update":{"name":"DSL Update","description":"Issues related to storing and updating the DSL","color":"e16cf3"},"AST-frontend":{"name":"AST-frontend","description":"Issues related to maintaining AST logic","color":"434a3a"},"AST-backend":{"name":"AST-backend","description":"Backend issues related to AST parsing","color":"c476eb"},"MariaDB":{"name":"MariaDB","description":"MariaDB datasource","color":"8428c3"},"Billing & Usage Pod":{"name":"Billing & Usage Pod","description":"Issues pertaining to licensing, billing, usage across self serve and enterprise customers","color":"256808"},"ADS Component Issue":{"name":"ADS Component Issue","description":"Issues which are caused due to ADS components","color":"d89119"},"Regressed":{"color":"723fd0","name":"Regressed","description":"Scenarios that were working before but have now regressed"},"Needs RCA":{"name":"Needs RCA","description":"a critical or high priority issue that needs an RCA","color":"2cc68f"},"Custom JS Libraries":{"name":"Custom JS Libraries","description":"Issues related to adding custom JS library","color":"bacb6d"},"Integrations Pod General":{"name":"Integrations Pod General","description":"Issues related to the Integrations Pod that don't fit into other tags.","color":"287823"},"Performance Pod":{"name":"Performance Pod","description":"All things related to Appsmith performance","color":"b5a25d"},"Performance":{"name":"Performance","description":"Issues related to performance","color":"9a18d7"},"File upload issues":{"name":"File upload issues","description":"Issues related to uploading any type of files from within Appsmith","color":"8154df"},"Action Selector":{"name":"Action Selector","description":"Issues related to action selector on the property pane","color":"2f9e20"},"Widget design system":{"name":"Widget design system","description":"","color":"cb6188"},"Deploy App":{"name":"Deploy App","description":"Issues related to app deployment","color":"6f6152"},"Community Reported":{"name":"Community Reported","description":"issues reported by community members","color":"1402e5"},"JS Function execution":{"name":"JS Function execution","description":"JS function execution","color":"7c2de1"},"Self Serve":{"name":"Self Serve","description":"For all issues related to self-serve flow for business edition","color":"4dacfc"},"Self Serve 1.0":{"name":"Self Serve 1.0","description":"For all issues related to v1 of the self serve project","color":"ae839e"},"CE Instance":{"name":"CE Instance","description":"For all issues relating to usage, licensing or billing on the CE instance","color":"d2bc40"},"Customer Portal":{"name":"Customer Portal","description":"For all tasks/issues pertaining to customer.appsmith.com","color":"d2bc40"},"Cloud Services":{"name":"Cloud Services","description":"For all tasks/issues on Appsmith cloud-services relating to licensing, usage and billing","color":"d2bc40"},"Billing Integrations":{"name":"Billing Integrations","description":"For all issues relating to 3P integrations Appsmith is using for billing & usage","color":"d2bc40"},"One-click Binding":{"name":"One-click Binding","description":"Issues related to the One click binding epic","color":"f1661c"},"Airgap":{"name":"Airgap","description":"Tickets related to supporting air-gapped Appsmith instances","color":"1cb294"},"SMTP plugin":{"name":"SMTP plugin","description":"Issues related to SMTP plugin","color":"541457"},"AWS AMI":{"name":"AWS AMI","description":"Issues Related to AWS AMI","color":"b44680"},"Old widget version":{"name":"Old widget version","description":"Use this label to raise issue specific only to an older version of a widget","color":"ff3814"},"Enterprise Billing":{"name":"Enterprise Billing","description":"To track all tasks/issues related to licensing & billing for enterprise customers","color":"14c156"},"Appsmith Business Cloud":{"name":"Appsmith Business Cloud","description":"Issues related to our business cloud offering","color":"89bb6c"},"Oracle SQL DB":{"name":"Oracle SQL DB","description":"Issues related to the Oracle plugin","color":"cbabcb"},"Community Contributor":{"name":"Community Contributor","description":"Meant to track issues that are assigned to external contributors","color":"149ab6"},"widget vertical alignment":{"name":"widget vertical alignment","description":"All issue related widget vertical alignment on the auto layout canvas","color":"d12d2e"},"Observability":{"name":"Observability","description":"Issues related to observability on the Appsmith instance","color":"dff913"},"Checkbox Component":{"name":"Checkbox Component","description":"This labels deals with checkbox component in wds package","color":"75a401"},"In-app ramps":{"name":"In-app ramps","description":"For all tasks/issues relating to adding in-app ramps in the community edition of the product","color":"8abae0"},"Analytics Improvements":{"name":"Analytics Improvements","description":"For all tasks focused on improving our overall analytics and fixing any issues ","color":"29b8ed"},"WDS team":{"name":"WDS team","description":"","color":"8d675a"},"Enterprise Edition":{"name":"Enterprise Edition","description":"Features that will be supported in Enterprise Edition only","color":"984f5e"},"Query filter":{"name":"Query filter","description":"Issues related to query filtering, e.g., WHERE clause","color":"a15134"},"Keyboard accessibility ":{"name":"Keyboard accessibility ","description":"All issue related to ADS component keyboard accessibility","color":"2ba696"},"Toggle button":{"name":"Toggle button","description":"All issue related to ADS toggle button","color":"edc47f"},"Feature Flagging":{"name":"Feature Flagging","description":"Anything related feature flagging","color":"8d8a09"},"SCIM":{"name":"SCIM","description":"Label to collate our SCIM issues","color":"61a852"},"ADS Category Token":{"name":"ADS Category Token","description":"All issues related appsmith design system category tokens","color":"920961"},"ADS Component Documentation":{"name":"ADS Component Documentation","description":"All issues Appsmith design system component documentation","color":"64c46a"},"ADS Migration":{"name":"ADS Migration","description":"All issues related to Appsmith design system migration","color":"b082d6"},"ADS Deduplication ":{"name":"ADS Deduplication ","description":"Replacing component with ADS components","color":"b082d6"},"ADS Revamp":{"name":"ADS Revamp","description":"All issues related to ads revamp. ","color":"b082d6"},"ADS Deduplication":{"name":"ADS Deduplication","description":"Replacing component with ADS components","color":"b082d6"},"ADS Grayscale":{"name":"ADS Grayscale","description":"Support grayscale color changes","color":"b03577"},"ADS Unit Test":{"name":"ADS Unit Test","description":"All issue related ads unit cases ","color":"b082d6"},"ADS Components":{"name":"ADS Components","description":"All issues related ADS components","color":"b082d6"},"Widget Discoverability":{"name":"Widget Discoverability","description":"Issues related to Widget Discoverability","color":"7b55ce"},"Widget setter method":{"name":"Widget setter method","description":"Issues with widget property setters","color":"8dce87"},"License":{"name":"License","description":"For all issues/tasks related to licensing of appsmith-ee edition","color":"90ee98"},"Templates pod":{"name":"Templates pod","description":"Issues related to Templates","color":"b7e568"},"Community template":{"name":"Community template","description":"Label for development of community templates and its integration to platform","color":"8a0510"},"DocumentDB":{"name":"DocumentDB","description":"Issues related to support DocumentDB in Appsmith Data layer","color":"2c8b56"},"Multiple Environments":{"name":"Multiple Environments","description":"Issues or tasks related to multiple environments","color":"4e972b"},"Platformization":{"name":"Platformization","description":"Issues or tasks related to platformization of Appsmith codebase","color":"4e972b"},"Activation - datasources":{"name":"Activation - datasources","description":"issues related to activation projects","color":"7c7ace"},"Partial-import-export":{"name":"Partial-import-export","description":"Label for granular reusability.","color":"1e439c"},"AI":{"name":"AI","description":"All tasks related to AI","color":"75c4ce"},"Custom environments":{"name":"Custom environments","description":"Issues with creating or working with custom environments","color":"2137d6"},"ADS Typography":{"name":"ADS Typography","description":"All issue related typographical changes","color":"2dbe8d"},"Auto Layout":{"name":"Auto Layout","description":"Issues relates to auto layout","color":"92cf8c"},"Heroku":{"name":"Heroku","description":"Issues related to Heroku","color":"a81b69"},"ADS Visual Styles":{"name":"ADS Visual Styles","description":"All issues related to ADS visual styles","color":"d3da89"},"ADS Component Design":{"name":"ADS Component Design","description":"All issue related to component design","color":"5cc91e"},"Modal Component":{"name":"Modal Component","description":"All issue related to ads modal component","color":"ee63f3"},"App setting":{"name":"App setting","description":"Related to app settings panel within the app","color":"144206"},"BE instance":{"name":"BE instance","description":"For all issues related to license, billing on BE instance","color":"ae8f98"},"Schema":{"name":"Schema","description":"Issues related to database schema","color":"c470c2"},"Fixed layout":{"name":"Fixed layout","description":"issues related to fixed layout","color":"b66681"},"Anvil layout":{"name":"Anvil layout","description":"issues related to the new layout system anvil","color":"722bf0"},"New Deployment Mode":{"name":"New Deployment Mode","description":"Support a new mode of deployment","color":"108033"},"Custom widgets":{"name":"Custom widgets","description":"For all issues related to the custom widget project","color":"c9db9c"},"IDE Pod":{"name":"IDE Pod","description":"https://app.zenhub.com/workspaces/new-developers-pod-60507ad1d4b98d00150a2858/board","color":"d3d248"},"TM_BU":{"name":"TM_BU","description":"The issues on Team Manager which needs to be taken up by Billing & Usage","color":"198cdf"},"Homepage Experience V2":{"name":"Homepage Experience V2","description":"Label for reporting new tasks and bug fixes related to revamped homepage experience","color":"c55d54"},"Appsmith Labs":{"name":"Appsmith Labs","description":"All things related to AI and other new initiatives ","color":"712d51"},"Customer Success":{"name":"Customer Success","description":"Issues that the success team cares about","color":"6ccabd"},"Invite flow":{"name":"Invite flow","description":"Invite users flow and any associated actions","color":"881b35"},"Invite users":{"name":"Invite users","description":"Invite users flow and any associated actions","color":"23e6d6"},"Workflows Pod":{"name":"Workflows Pod","description":"For all issues related to the Workflows feature","color":"2c1f93"},"DailyPromotionBlocker":{"name":"DailyPromotionBlocker","description":"DailyPromotion Blocker","color":"9b2280"}},"success":true} \ No newline at end of file diff --git a/.github/workflows/test-build-docker-image.yml b/.github/workflows/test-build-docker-image.yml index c93b10b3d8..0ca0c30b85 100644 --- a/.github/workflows/test-build-docker-image.yml +++ b/.github/workflows/test-build-docker-image.yml @@ -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 \ diff --git a/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/RepoLimitExceededErrorModal_spec.js b/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/RepoLimitExceededErrorModal_spec.js index 097c6a5249..7f67821435 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/RepoLimitExceededErrorModal_spec.js +++ b/app/client/cypress/e2e/Regression/ClientSide/Git/GitSync/RepoLimitExceededErrorModal_spec.js @@ -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(); diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Chart/Chart_widget_spec_2.ts b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Chart/Chart_widget_spec_2.ts index ea912be917..e06da685d2 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Chart/Chart_widget_spec_2.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Chart/Chart_widget_spec_2.ts @@ -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"), ); diff --git a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Chart/Custom3DChartSpec.ts b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Chart/Custom3DChartSpec.ts index efd713bdab..c5841d32cc 100644 --- a/app/client/cypress/e2e/Regression/ClientSide/Widgets/Chart/Custom3DChartSpec.ts +++ b/app/client/cypress/e2e/Regression/ClientSide/Widgets/Chart/Custom3DChartSpec.ts @@ -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"); diff --git a/app/client/cypress/snapshots/Custom3DChartSpec.ts/2DCustomECharts.snap.png b/app/client/cypress/snapshots/Custom3DChartSpec.ts/2DCustomECharts.snap.png index f2051e9535..d70c91094b 100644 Binary files a/app/client/cypress/snapshots/Custom3DChartSpec.ts/2DCustomECharts.snap.png and b/app/client/cypress/snapshots/Custom3DChartSpec.ts/2DCustomECharts.snap.png differ diff --git a/app/client/cypress/snapshots/Custom3DChartSpec.ts/3DCustomECharts-2.snap.png b/app/client/cypress/snapshots/Custom3DChartSpec.ts/3DCustomECharts-2.snap.png index 033dba7a32..3d25413697 100644 Binary files a/app/client/cypress/snapshots/Custom3DChartSpec.ts/3DCustomECharts-2.snap.png and b/app/client/cypress/snapshots/Custom3DChartSpec.ts/3DCustomECharts-2.snap.png differ diff --git a/app/client/cypress/snapshots/Custom3DChartSpec.ts/3DCustomECharts.snap.png b/app/client/cypress/snapshots/Custom3DChartSpec.ts/3DCustomECharts.snap.png index e36a68830d..c0bea562a5 100644 Binary files a/app/client/cypress/snapshots/Custom3DChartSpec.ts/3DCustomECharts.snap.png and b/app/client/cypress/snapshots/Custom3DChartSpec.ts/3DCustomECharts.snap.png differ diff --git a/app/client/cypress/snapshots/Custom3DChartSpec.ts/FusionCharts.snap.png b/app/client/cypress/snapshots/Custom3DChartSpec.ts/FusionCharts.snap.png index 01cb650758..99ca77a8d0 100644 Binary files a/app/client/cypress/snapshots/Custom3DChartSpec.ts/FusionCharts.snap.png and b/app/client/cypress/snapshots/Custom3DChartSpec.ts/FusionCharts.snap.png differ diff --git a/app/client/package.json b/app/client/package.json index a7a1a62ce1..19ae67cb28 100644 --- a/app/client/package.json +++ b/app/client/package.json @@ -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", diff --git a/app/client/src/assets/icons/templates/canvas-starter-record-details.svg b/app/client/src/assets/icons/templates/canvas-starter-record-details.svg new file mode 100644 index 0000000000..a99ab3a45f --- /dev/null +++ b/app/client/src/assets/icons/templates/canvas-starter-record-details.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/client/src/assets/icons/templates/canvas-starter-record-edit.svg b/app/client/src/assets/icons/templates/canvas-starter-record-edit.svg new file mode 100644 index 0000000000..1ddd1825ad --- /dev/null +++ b/app/client/src/assets/icons/templates/canvas-starter-record-edit.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/client/src/assets/icons/templates/canvas-starter-sort-filter-table.svg b/app/client/src/assets/icons/templates/canvas-starter-sort-filter-table.svg new file mode 100644 index 0000000000..4251d635c9 --- /dev/null +++ b/app/client/src/assets/icons/templates/canvas-starter-sort-filter-table.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/client/src/assets/icons/templates/starter-template-dashboard.svg b/app/client/src/assets/icons/templates/starter-template-dashboard.svg deleted file mode 100644 index 7ca68acc1b..0000000000 --- a/app/client/src/assets/icons/templates/starter-template-dashboard.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/client/src/assets/icons/templates/starter-template-form.svg b/app/client/src/assets/icons/templates/starter-template-form.svg deleted file mode 100644 index a1b35aaca7..0000000000 --- a/app/client/src/assets/icons/templates/starter-template-form.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/app/client/src/assets/icons/templates/starter-template-record-details.svg b/app/client/src/assets/icons/templates/starter-template-record-details.svg deleted file mode 100644 index 7fdb6e5930..0000000000 --- a/app/client/src/assets/icons/templates/starter-template-record-details.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/client/src/assets/icons/templates/starter-template-record-edit.svg b/app/client/src/assets/icons/templates/starter-template-record-edit.svg deleted file mode 100644 index 80f8a3d5e9..0000000000 --- a/app/client/src/assets/icons/templates/starter-template-record-edit.svg +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/client/src/ce/constants/messages.ts b/app/client/src/ce/constants/messages.ts index c7e018b77c..5e29aa561f 100644 --- a/app/client/src/ce/constants/messages.ts +++ b/app/client/src/ce/constants/messages.ts @@ -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:", diff --git a/app/client/src/ce/entities/FeatureFlag.ts b/app/client/src/ce/entities/FeatureFlag.ts index 7cec6acf88..966c5109d6 100644 --- a/app/client/src/ce/entities/FeatureFlag.ts +++ b/app/client/src/ce/entities/FeatureFlag.ts @@ -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, diff --git a/app/client/src/ce/selectors/appIDESelectors.ts b/app/client/src/ce/selectors/appIDESelectors.ts index 5a9ebca107..edfcfe5434 100644 --- a/app/client/src/ce/selectors/appIDESelectors.ts +++ b/app/client/src/ce/selectors/appIDESelectors.ts @@ -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) => { diff --git a/app/client/src/ce/selectors/entitiesSelector.ts b/app/client/src/ce/selectors/entitiesSelector.ts index 7f6bdcaad8..68ab7551f0 100644 --- a/app/client/src/ce/selectors/entitiesSelector.ts +++ b/app/client/src/ce/selectors/entitiesSelector.ts @@ -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; + }, +); diff --git a/app/client/src/ce/utils/analyticsUtilTypes.ts b/app/client/src/ce/utils/analyticsUtilTypes.ts index 5c586903af..832299c110 100644 --- a/app/client/src/ce/utils/analyticsUtilTypes.ts +++ b/app/client/src/ce/utils/analyticsUtilTypes.ts @@ -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"; diff --git a/app/client/src/components/editorComponents/ActionCreator/helpers.tsx b/app/client/src/components/editorComponents/ActionCreator/helpers.tsx index 359e3ca3ae..066c761f62 100644 --- a/app/client/src/components/editorComponents/ActionCreator/helpers.tsx +++ b/app/client/src/components/editorComponents/ActionCreator/helpers.tsx @@ -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: , args: argValue, diff --git a/app/client/src/constants/TemplatesConstants.tsx b/app/client/src/constants/TemplatesConstants.tsx index c7288d20ab..f53726e28d 100644 --- a/app/client/src/constants/TemplatesConstants.tsx +++ b/app/client/src/constants/TemplatesConstants.tsx @@ -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: , + icon: , 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", }, ], }; diff --git a/app/client/src/entities/Widget/utils.test.ts b/app/client/src/entities/Widget/utils.test.ts index 1e3eee539c..8ae7211d75 100644 --- a/app/client/src/entities/Widget/utils.test.ts +++ b/app/client/src/entities/Widget/utils.test.ts @@ -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, diff --git a/app/client/src/layoutSystems/common/dropTarget/starterBuildingBlocks/StyledComponents.tsx b/app/client/src/layoutSystems/common/dropTarget/starterBuildingBlocks/StyledComponents.tsx index 32b0ed5224..132faaf4ac 100644 --- a/app/client/src/layoutSystems/common/dropTarget/starterBuildingBlocks/StyledComponents.tsx +++ b/app/client/src/layoutSystems/common/dropTarget/starterBuildingBlocks/StyledComponents.tsx @@ -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"}; `; diff --git a/app/client/src/layoutSystems/common/dropTarget/starterBuildingBlocks/index.tsx b/app/client/src/layoutSystems/common/dropTarget/starterBuildingBlocks/index.tsx index 25e247a3e9..9c1e714e71 100644 --- a/app/client/src/layoutSystems/common/dropTarget/starterBuildingBlocks/index.tsx +++ b/app/client/src/layoutSystems/common/dropTarget/starterBuildingBlocks/index.tsx @@ -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)} > - + {createMessage(STARTER_TEMPLATE_PAGE_LAYOUTS.header)} - + {layoutItems.map((item, index) => ( @@ -155,12 +159,27 @@ function StarterBuildingBlocks() { - + {item.title} - - + + {item.description} - + ))} diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/blank.ts b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/blank.ts deleted file mode 100644 index 4b930114d7..0000000000 --- a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/blank.ts +++ /dev/null @@ -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: "", - }, -}; diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/index.ts b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/index.ts index 9825b6ec1e..8174339fad 100644 --- a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/index.ts +++ b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/index.ts @@ -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]; diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/react.ts b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/react.ts index 8f944018c8..251c42d166 100644 --- a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/react.ts +++ b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/react.ts @@ -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 ( - +

Custom Widget

@@ -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(, document.getElementById("root")); -});`, - }, - srcDoc: { - html: ` -
-`, - 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")); });`, }, }; diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/vanillaJs.ts b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/vanillaJs.ts new file mode 100644 index 0000000000..e86786d4c6 --- /dev/null +++ b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/vanillaJs.ts @@ -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: `
+
+
+

Custom Widget

+
+
+
+
+
+ + +
+
`, + 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(); +});`, + }, +}; diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/vue.ts b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/vue.ts index 8506b6b9c4..440e54d3a5 100644 --- a/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/vue.ts +++ b/app/client/src/pages/Editor/CustomWidgetBuilder/Editor/Header/CodeTemplates/Templates/vue.ts @@ -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: `
-

{{ msg }}

+ html: `
+
+
+

Custom Widget

+
{{ currentIndex + 1 }} / {{ tips.length }}
+
+
{{ tips[currentIndex] }}
+
+
+ + +
-`, - css: `#hello-world-app { - font-family: monospace; - width: 100vw; - height: 100vh; - display: flex; - justify-content: center; - align-items: center; +`, + 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"); + }, + }, + }); });`, }, }; diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/constants.ts b/app/client/src/pages/Editor/CustomWidgetBuilder/constants.ts index 42941c6bbd..23a74e2500 100644 --- a/app/client/src/pages/Editor/CustomWidgetBuilder/constants.ts +++ b/app/client/src/pages/Editor/CustomWidgetBuilder/constants.ts @@ -21,14 +21,14 @@ export const DEFAULT_CONTEXT_VALUE = { name: "", widgetId: "", srcDoc: { - html: "
Hello World
", - js: "function test() {console.log('Hello World');}", - css: "div {color: red;}", + html: "", + js: "", + css: "", }, uncompiledSrcDoc: { - html: "
Hello World
", - 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"; diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/utility.test.ts b/app/client/src/pages/Editor/CustomWidgetBuilder/utility.test.ts index e1abfb4e7b..fa44efd508 100644 --- a/app/client/src/pages/Editor/CustomWidgetBuilder/utility.test.ts +++ b/app/client/src/pages/Editor/CustomWidgetBuilder/utility.test.ts @@ -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: "
Hello World
", - js: "const a = 5 )", + js: "appsmith.onReady(() => {const a = 5 )})", css: "div { color: red; }", }; diff --git a/app/client/src/pages/Editor/CustomWidgetBuilder/utility.ts b/app/client/src/pages/Editor/CustomWidgetBuilder/utility.ts index 0a2f3f0b32..9803273187 100644 --- a/app/client/src/pages/Editor/CustomWidgetBuilder/utility.ts +++ b/app/client/src/pages/Editor/CustomWidgetBuilder/utility.ts @@ -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; diff --git a/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx b/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx index 7411db03b2..253a9e513e 100644 --- a/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx +++ b/app/client/src/pages/Editor/DataSourceEditor/NewActionButton.tsx @@ -125,10 +125,10 @@ function NewActionButton(props: NewActionButtonProps) { height={pages.length <= 4 ? "fit-content" : "186px"} side={"bottom"} > - {`Create a ${ + {`Create ${ pluginType === PluginType.DB || pluginType === PluginType.SAAS ? "query" - : "api" + : "API" } in`} {pageMenuItems.map((page, i) => { if (page) { diff --git a/app/client/src/pages/Editor/IDE/EditorTabs/FileTabs.tsx b/app/client/src/pages/Editor/IDE/EditorTabs/FileTabs.tsx index b1622209bd..2d6f6b4e9a 100644 --- a/app/client/src/pages/Editor/IDE/EditorTabs/FileTabs.tsx +++ b/app/client/src/pages/Editor/IDE/EditorTabs/FileTabs.tsx @@ -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, diff --git a/app/client/src/pages/Editor/IDE/EditorTabs/JSTab.tsx b/app/client/src/pages/Editor/IDE/EditorTabs/JSTab.tsx index aae38276a4..a96ef3d3b4 100644 --- a/app/client/src/pages/Editor/IDE/EditorTabs/JSTab.tsx +++ b/app/client/src/pages/Editor/IDE/EditorTabs/JSTab.tsx @@ -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"; diff --git a/app/client/src/pages/Editor/IDE/EditorTabs/QueryTab.tsx b/app/client/src/pages/Editor/IDE/EditorTabs/QueryTab.tsx index d9d1956b69..9012d537d2 100644 --- a/app/client/src/pages/Editor/IDE/EditorTabs/QueryTab.tsx +++ b/app/client/src/pages/Editor/IDE/EditorTabs/QueryTab.tsx @@ -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"; diff --git a/app/client/src/sagas/ActionSagas.ts b/app/client/src/sagas/ActionSagas.ts index 363026151c..d062d4b92d 100644 --- a/app/client/src/sagas/ActionSagas.ts +++ b/app/client/src/sagas/ActionSagas.ts @@ -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, diff --git a/app/client/src/widgets/ChartWidget/constants.ts b/app/client/src/widgets/ChartWidget/constants.ts index 27aac52ca7..560efd9faf 100644 --- a/app/client/src/widgets/ChartWidget/constants.ts +++ b/app/client/src/widgets/ChartWidget/constants.ts @@ -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", diff --git a/app/client/src/widgets/ChartWidget/widget/index.test.ts b/app/client/src/widgets/ChartWidget/widget/index.test.ts index 0a7b039960..2ef5def288 100644 --- a/app/client/src/widgets/ChartWidget/widget/index.test.ts +++ b/app/client/src/widgets/ChartWidget/widget/index.test.ts @@ -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"; diff --git a/app/client/src/widgets/ChartWidget/widget/index.tsx b/app/client/src/widgets/ChartWidget/widget/index.tsx index edd0636e0f..1f1cd544a0 100644 --- a/app/client/src/widgets/ChartWidget/widget/index.tsx +++ b/app/client/src/widgets/ChartWidget/widget/index.tsx @@ -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 { 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 { } 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}}", diff --git a/app/client/src/widgets/ChartWidget/widget/propertyConfig.test.ts b/app/client/src/widgets/ChartWidget/widget/propertyConfig.test.ts index 0e7e74fd9e..1bcb8524a0 100644 --- a/app/client/src/widgets/ChartWidget/widget/propertyConfig.test.ts +++ b/app/client/src/widgets/ChartWidget/widget/propertyConfig.test.ts @@ -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 { diff --git a/app/client/src/widgets/ChartWidget/widget/propertyConfig.ts b/app/client/src/widgets/ChartWidget/widget/propertyConfig.ts index e289d6ad2a..23f814bb43 100644 --- a/app/client/src/widgets/ChartWidget/widget/propertyConfig.ts +++ b/app/client/src/widgets/ChartWidget/widget/propertyConfig.ts @@ -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, diff --git a/app/client/src/widgets/CustomWidget/component/appsmithConsole.js b/app/client/src/widgets/CustomWidget/component/appsmithConsole.js index 1c27b1b5eb..1f68e5ed4a 100644 --- a/app/client/src/widgets/CustomWidget/component/appsmithConsole.js +++ b/app/client/src/widgets/CustomWidget/component/appsmithConsole.js @@ -25,7 +25,7 @@ }, }); - ["log", "warn", "info"].forEach((method) => { + ["log", "warn", "info", "error"].forEach((method) => { nativeConsole[method] = createProxy(method); }); diff --git a/app/client/src/widgets/CustomWidget/component/index.tsx b/app/client/src/widgets/CustomWidget/component/index.tsx index 82f29e6b0a..6ece788f7d 100644 --- a/app/client/src/widgets/CustomWidget/component/index.tsx +++ b/app/client/src/widgets/CustomWidget/component/index.tsx @@ -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, }; diff --git a/app/client/src/widgets/CustomWidget/widget/defaultApp.ts b/app/client/src/widgets/CustomWidget/widget/defaultApp.ts index d98fb154eb..4451d46fd0 100644 --- a/app/client/src/widgets/CustomWidget/widget/defaultApp.ts +++ b/app/client/src/widgets/CustomWidget/widget/defaultApp.ts @@ -1,3 +1,5 @@ +import { CUSTOM_WIDGET_ONREADY_DOC_URL } from "pages/Editor/CustomWidgetBuilder/constants"; + export default { uncompiledSrcDoc: { html: ` @@ -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(, document.getElementById("root")); });`, }, diff --git a/app/client/src/widgets/CustomWidget/widget/index.tsx b/app/client/src/widgets/CustomWidget/widget/index.tsx index 2b370b5ff0..d943782935 100644 --- a/app/client/src/widgets/CustomWidget/widget/index.tsx +++ b/app/client/src/widgets/CustomWidget/widget/index.tsx @@ -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 { }, globalContext: contextObj, }); + + AnalyticsUtil.logEvent("CUSTOM_WIDGET_API_TRIGGER_EVENT", { + widgetId: this.props.widgetId, + eventName, + }); } }; @@ -340,6 +346,10 @@ class CustomWidget extends BaseWidget { ...this.props.model, ...data, }); + + AnalyticsUtil.logEvent("CUSTOM_WIDGET_API_UPDATE_MODEL", { + widgetId: this.props.widgetId, + }); }; getRenderMode = () => { diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 6b8bd23d71..155fe79eab 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -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 diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/actioncollections/imports/ActionCollectionImportableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/actioncollections/imports/ActionCollectionImportableServiceCEImpl.java index e0dd9efc89..f0c541c5e1 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/actioncollections/imports/ActionCollectionImportableServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/actioncollections/imports/ActionCollectionImportableServiceCEImpl.java @@ -75,12 +75,13 @@ public class ActionCollectionImportableServiceCEImpl implements ImportableServic MappedImportableResourcesDTO mappedImportableResourcesDTO) { Mono> importedActionCollectionMono = Mono.just(importedActionCollectionList); - if (importingMetaDTO.getAppendToApp()) { + if (importingMetaDTO.getAppendToArtifact()) { importedActionCollectionMono = importedActionCollectionMono.map(importedActionCollectionList1 -> { - List importedNewPages = mappedImportableResourcesDTO.getPageNameMap().values().stream() + List importedNewPages = mappedImportableResourcesDTO.getPageOrModuleMap().values().stream() .distinct() + .map(branchAwareDomain -> (NewPage) branchAwareDomain) .toList(); - Map newToOldNameMap = mappedImportableResourcesDTO.getNewPageNameToOldPageNameMap(); + Map 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) + 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) + mappedImportableResourcesDTO.getPageOrModuleMap()); parentPage = parentPage == null ? publishedCollectionPage : parentPage; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationImportServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationImportServiceCEImpl.java new file mode 100644 index 0000000000..0cec2011f0 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationImportServiceCEImpl.java @@ -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 themeImportableService; + private final ImportableService newPageImportableService; + private final ImportableService customJSLibImportableService; + private final ImportableService newActionImportableService; + private final ImportableService 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 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 themeImportableService, + ImportableService newPageImportableService, + ImportableService customJSLibImportableService, + ImportableService newActionImportableService, + ImportableService 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() {}.getType(); + return gson.fromJson(jsonString, fileType); + } + + @Override + public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForImportingArtifact( + Set userPermissionGroups) { + return ImportArtifactPermissionProvider.builder( + applicationPermission, + pagePermission, + actionPermission, + datasourcePermission, + workspacePermission) + .requiredPermissionOnTargetWorkspace(workspacePermission.getApplicationCreatePermission()) + .permissionRequiredToCreateDatasource(true) + .permissionRequiredToEditDatasource(true) + .currentUserPermissionGroups(userPermissionGroups) + .build(); + } + + @Override + public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForUpdatingArtifact( + Set 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 userPermissions) { + return ImportArtifactPermissionProvider.builder( + applicationPermission, + pagePermission, + actionPermission, + datasourcePermission, + workspacePermission) + .requiredPermissionOnTargetApplication(applicationPermission.getEditPermission()) + .currentUserPermissionGroups(userPermissions) + .build(); + } + + @Override + public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForRestoringSnapshot( + Set userPermissions) { + return ImportArtifactPermissionProvider.builder( + applicationPermission, + pagePermission, + actionPermission, + datasourcePermission, + workspacePermission) + .requiredPermissionOnTargetWorkspace(workspacePermission.getReadPermission()) + .requiredPermissionOnTargetApplication(applicationPermission.getEditPermission()) + .currentUserPermissionGroups(userPermissions) + .build(); + } + + @Override + public ImportArtifactPermissionProvider getImportArtifactPermissionProviderForMergingJsonWithArtifact( + Set 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> getPageDependentImportables( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono 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 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 importedActionCollectionsMono = actionCollectionImportableService.importEntities( + importingMetaDTO, + mappedImportableResourcesDTO, + workspaceMono, + importedApplicationMono, + applicationJson, + false); + + Mono combinedActionImportablesMono = importedNewActionsMono.then(importedActionCollectionsMono); + return List.of(combinedActionImportablesMono); + } + + @Override + public Mono getImportableArtifactDTO( + String workspaceId, String applicationId, ImportableArtifact importableArtifact) { + Application application = (Application) importableArtifact; + return findDatasourceByApplicationId(applicationId, workspaceId) + .zipWith(workspaceService.getDefaultEnvironmentId(workspaceId, null)) + .map(tuple2 -> { + List 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> findDatasourceByApplicationId(String applicationId, String workspaceId) { + // TODO: Investigate further why datasourcePermission.getReadPermission() is not being used. + Mono> listMono = datasourceService + .getAllByWorkspaceIdWithStorages(workspaceId, Optional.empty()) + .collectList(); + return newActionService + .findAllByApplicationIdAndViewMode(applicationId, false, Optional.empty(), Optional.empty()) + .collectList() + .zipWith(listMono) + .flatMap(objects -> { + List datasourceList = objects.getT2(); + List actionList = objects.getT1(); + List 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 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 applicationPageList = + new ArrayList<>(applicationJson.getPageList().size()); + List pageNames = + new ArrayList<>(applicationJson.getPageList().size()); + List 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 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 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 sendImportExportApplicationAnalyticsEvent( + Application application, AnalyticsEvents event) { + return workspaceService.getById(application.getWorkspaceId()).flatMap(workspace -> { + final Map eventData = Map.of( + FieldName.APPLICATION, application, + FieldName.WORKSPACE, workspace); + + final Map 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 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 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 isArtifactConnectedToGit(String artifactId) { + return applicationService.isApplicationConnectedToGit(artifactId); + } + + @Override + public Mono updateAndSaveArtifactInContext( + ImportableArtifact importableArtifact, + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono currentUserMono) { + Mono 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> 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 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 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 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 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 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 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 generateArtifactContextIndependentImportableEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono 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 importedPagesMono = newPageImportableService.importEntities( + importingMetaDTO, + mappedImportableResourcesDTO, + workspaceMono, + Mono.just(application), + applicationJson, + false); + + // Directly updates required theme information in DB + Mono importedThemesMono = themeImportableService.importEntities( + importingMetaDTO, + mappedImportableResourcesDTO, + workspaceMono, + Mono.just(application), + applicationJson, + false, + true); + + return Flux.merge(List.of(importedPagesMono, importedThemesMono)); + }); + } + + @Override + public Flux generateArtifactContextDependentImportableEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono importableArtifactMono, + ArtifactExchangeJson artifactExchangeJson) { + + return importableArtifactMono.flatMapMany(importableContext -> { + Application application = (Application) importableContext; + ApplicationJson applicationJson = (ApplicationJson) artifactExchangeJson; + + List> 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 getArtifactSpecificConstantsMap() { + return applicationConstantsMap; + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationImportServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationImportServiceImpl.java new file mode 100644 index 0000000000..04dead67bf --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/base/ApplicationImportServiceImpl.java @@ -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 themeImportableService, + ImportableService newPageImportableService, + ImportableService customJSLibImportableService, + ImportableService newActionImportableService, + ImportableService actionCollectionImportableService) { + super( + datasourceService, + workspaceService, + applicationService, + applicationPageService, + newActionService, + analyticsService, + datasourcePermission, + workspacePermission, + applicationPermission, + pagePermission, + actionPermission, + gson, + themeImportableService, + newPageImportableService, + customJSLibImportableService, + newActionImportableService, + actionCollectionImportableService); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportService.java new file mode 100644 index 0000000000..8804065eb9 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportService.java @@ -0,0 +1,3 @@ +package com.appsmith.server.applications.imports; + +public interface ApplicationImportService extends ApplicationImportServiceCE {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceCE.java new file mode 100644 index 0000000000..99d17d82fc --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/applications/imports/ApplicationImportServiceCE.java @@ -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 { + + Mono> findDatasourceByApplicationId(String applicationId, String orgId); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ArtifactJsonType.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ArtifactJsonType.java new file mode 100644 index 0000000000..b26aff45b4 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ArtifactJsonType.java @@ -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 +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ce/FieldNameCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ce/FieldNameCE.java index 041365f32c..c36ec7803f 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ce/FieldNameCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/constants/ce/FieldNameCE.java @@ -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"; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java index b699de97f8..495c0624f4 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ApplicationController.java @@ -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); } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationControllerCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationControllerCE.java index a05ec6e194..ef798c9228 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationControllerCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/controllers/ce/ApplicationControllerCE.java @@ -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> importApplicationFromFile( + public Mono> importApplicationFromFile( @RequestPart("file") Mono fileMono, @PathVariable String workspaceId, @RequestParam(name = FieldName.APPLICATION_ID, required = false) String applicationId) { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/imports/DatasourceImportableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/imports/DatasourceImportableServiceCEImpl.java index 24b84c7bb5..faa879a7d2 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/imports/DatasourceImportableServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/datasources/imports/DatasourceImportableServiceCEImpl.java @@ -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 importEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono 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> existingDatasourceMono = - getExistingDatasourceMono(importingMetaDTO.getApplicationId(), existingDatasourceFlux); + getExistingDatasourceMono(importingMetaDTO.getArtifactId(), existingDatasourceFlux); Mono> datasourceMapMono = importDatasources( applicationJson, existingDatasourceMono, @@ -247,7 +271,7 @@ public class DatasourceImportableServiceCEImpl implements ImportableServiceCE unConfiguredDatasourceList; diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ArtifactExchangeJson.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ArtifactExchangeJson.java new file mode 100644 index 0000000000..3e5c75c2a5 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ArtifactExchangeJson.java @@ -0,0 +1,5 @@ +package com.appsmith.server.dtos; + +import com.appsmith.server.dtos.ce.ArtifactExchangeJsonCE; + +public interface ArtifactExchangeJson extends ArtifactExchangeJsonCE {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ImportableArtifactDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ImportableArtifactDTO.java new file mode 100644 index 0000000000..32b8f2606e --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ImportableArtifactDTO.java @@ -0,0 +1,3 @@ +package com.appsmith.server.dtos; + +public abstract class ImportableArtifactDTO {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ImportingMetaDTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ImportingMetaDTO.java index 6ea04a37f8..1cd11b253d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ImportingMetaDTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ImportingMetaDTO.java @@ -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 currentUserPermissionGroups; } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/ApplicationJsonCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/ApplicationJsonCE.java index b366c04c6a..81d380131d 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/ApplicationJsonCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/ApplicationJsonCE.java @@ -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 getCustomJsLibFromArtifact() { + return this.getCustomJSLibList(); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/ArtifactExchangeJsonCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/ArtifactExchangeJsonCE.java new file mode 100644 index 0000000000..520af3f880 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/ArtifactExchangeJsonCE.java @@ -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 getCustomJsLibFromArtifact(); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/MappedImportableResourcesCE_DTO.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/MappedImportableResourcesCE_DTO.java index 55bec0245f..d0d8d015cf 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/MappedImportableResourcesCE_DTO.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/dtos/ce/MappedImportableResourcesCE_DTO.java @@ -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 pluginMap = new HashMap<>(); Map datasourceNameToIdMap = new HashMap<>(); + // Artifact dependent + // This attribute is re-usable across artifacts according to the needs + Map 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 pageOrModuleMap; + + // Artifact dependent and common List installedJsLibsList; - Map newPageNameToOldPageNameMap; - Map pageNameMap; ImportActionResultDTO actionResultDTO; ImportActionCollectionResultDTO actionCollectionResultDTO; ImportedActionAndCollectionMapsDTO actionAndCollectionMapsDTO = new ImportedActionAndCollectionMapsDTO(); + + // This is being used to carry the resources from ArtifactExchangeJson + Map resourceStoreFromArtifactExchangeJson = new HashMap<>(); } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/exports/internal/PartialExportServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/exports/internal/PartialExportServiceCEImpl.java index ea103a46be..80f99d269b 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/exports/internal/PartialExportServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/exports/internal/PartialExportServiceCEImpl.java @@ -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 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 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 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 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( diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/ImportApplicationPermissionProvider.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/ImportArtifactPermissionProvider.java similarity index 91% rename from app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/ImportApplicationPermissionProvider.java rename to app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/ImportArtifactPermissionProvider.java index b4ed7061d0..2295fdaf34 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/ImportApplicationPermissionProvider.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/helpers/ce/ImportArtifactPermissionProvider.java @@ -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, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportService.java new file mode 100644 index 0000000000..ad0d620388 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportService.java @@ -0,0 +1,3 @@ +package com.appsmith.server.imports.importable; + +public interface ImportService extends ImportServiceCE {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportServiceCE.java new file mode 100644 index 0000000000..e488c603ea --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportServiceCE.java @@ -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 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 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 importNewArtifactInWorkspaceFromJson( + String workspaceId, ArtifactExchangeJson artifactExchangeJson); + + Mono 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 importArtifactInWorkspaceFromGit( + String workspaceId, String artifactId, ArtifactExchangeJson artifactExchangeJson, String branchName); + + Mono mergeArtifactExchangeJsonWithImportableArtifact( + String workspaceId, + String artifactId, + String branchName, + ArtifactExchangeJson artifactExchangeJson, + List entitiesToImport); + + Mono restoreSnapshot( + String workspaceId, ArtifactExchangeJson artifactExchangeJson, String artifactId, String branchName); + + Mono getArtifactImportDTO( + String workspaceId, + String artifactId, + ImportableArtifact importableArtifact, + ArtifactExchangeJson artifactExchangeJson); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportableServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportableServiceCE.java index 05ac52dcf6..4397f5f922 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportableServiceCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/importable/ImportableServiceCE.java @@ -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 { boolean isPartialImport) { return null; } + + default Mono importEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono importContextMono, + ArtifactExchangeJson importableContextJson, + boolean isPartialImport, + boolean isContextAgnostic) { + return null; + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ContextBasedImportService.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ContextBasedImportService.java new file mode 100644 index 0000000000..f3c3fc93c2 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ContextBasedImportService.java @@ -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 {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ContextBasedImportServiceCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ContextBasedImportServiceCE.java new file mode 100644 index 0000000000..cfea6521b5 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ContextBasedImportServiceCE.java @@ -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 userPermissions); + + ImportArtifactPermissionProvider getImportArtifactPermissionProviderForUpdatingArtifact( + Set userPermissions); + + ImportArtifactPermissionProvider getImportArtifactPermissionProviderForConnectingToGit(Set userPermissions); + + ImportArtifactPermissionProvider getImportArtifactPermissionProviderForRestoringSnapshot( + Set userPermissions); + + ImportArtifactPermissionProvider getImportArtifactPermissionProviderForMergingJsonWithArtifact( + Set 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 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 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 updateAndSaveArtifactInContext( + ImportableArtifact importableArtifact, + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono currentUserMono); + + /** + * update importable entities with the context references post creation of context in db + * @param importableContext + * @param mappedImportableResourcesDTO + * @param importingMetaDTO + * @return + */ + Mono 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 updateImportableArtifact(ImportableArtifact importableArtifact); + + Map createImportAnalyticsData( + ArtifactExchangeJson artifactExchangeJson, ImportableArtifact importableArtifact); + + /** + * @param importingMetaDTO + * @param mappedImportableResourcesDTO + * @param workspaceMono + * @param importableArtifactMono + * @param artifactExchangeJson + * @return + */ + Flux generateArtifactContextIndependentImportableEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono importableArtifactMono, + ArtifactExchangeJson artifactExchangeJson); + + /** + * @param importingMetaDTO + * @param mappedImportableResourcesDTO + * @param workspaceMono + * @param importableArtifactMono + * @param artifactExchangeJson + * @return + */ + Flux generateArtifactContextDependentImportableEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono importableArtifactMono, + ArtifactExchangeJson artifactExchangeJson); + + /** + * Add entities which are specific to the artifact. i.e. customJsLib + * @param artifactExchangeJson + * @param importingMetaDTO + * @param mappedImportableResourcesDTO + * @return + */ + Mono generateArtifactSpecificImportableEntities( + ArtifactExchangeJson artifactExchangeJson, + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO); + + Mono 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 getArtifactSpecificConstantsMap(); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportApplicationServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportApplicationServiceCEImpl.java index a994819a7a..c833e135a5 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportApplicationServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportApplicationServiceCEImpl.java @@ -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 getPermissionProviderForUpdateNonGitConnectedAppFromJson() { + private Mono 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 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 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 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, diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCEImpl.java new file mode 100644 index 0000000000..7665e2055e --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceCEImpl.java @@ -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 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 customJSLibImportableService; + private final PermissionGroupRepository permissionGroupRepository; + private final TransactionalOperator transactionalOperator; + private final AnalyticsService analyticsService; + private final ImportableService pluginImportableService; + private final ImportableService datasourceImportableService; + private final ImportableService themeImportableService; + private final Map> serviceFactory = new HashMap<>(); + + public ImportServiceCEImpl( + ApplicationImportService applicationImportService, + SessionUserService sessionUserService, + WorkspaceService workspaceService, + ImportableService customJSLibImportableService, + PermissionGroupRepository permissionGroupRepository, + TransactionalOperator transactionalOperator, + AnalyticsService analyticsService, + ImportableService pluginImportableService, + ImportableService datasourceImportableService, + ImportableService 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 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 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 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 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 userPermissionGroup = tuple2.getT1(); + ImportArtifactPermissionProvider permissionProvider = tuple2.getT2(); + return importArtifactInWorkspace( + workspaceId, + artifactExchangeJson, + null, + null, + false, + permissionProvider, + userPermissionGroup); + }); + } + + @Override + public Mono 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 isArtifactConnectedToGitMono = Mono.just(Boolean.FALSE); + if (!StringUtils.isEmpty(artifactId)) { + isArtifactConnectedToGitMono = contextBasedImportService.isArtifactConnectedToGit(artifactId); + } + + Mono 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 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 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 userPermissionGroup = tuple2.getT1(); + ImportArtifactPermissionProvider artifactPermissionProvider = tuple2.getT2(); + return importArtifactInWorkspace( + workspaceId, + artifactExchangeJson, + artifactId, + branchName, + false, + artifactPermissionProvider, + userPermissionGroup); + }); + } + + @Override + public Mono 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 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 mergeArtifactExchangeJsonWithImportableArtifact( + String workspaceId, + String artifactId, + String branchName, + ArtifactExchangeJson artifactExchangeJson, + List entitiesToImport) { + ContextBasedImportService contextBasedImportService = + getContextBasedImportService(artifactExchangeJson); + contextBasedImportService.updateArtifactExchangeJsonWithEntitiesToBeConsumed( + artifactExchangeJson, entitiesToImport); + return permissionGroupRepository + .getCurrentUserPermissionGroups() + .zipWhen(userPermissionGroups -> { + return Mono.just( + contextBasedImportService.getImportArtifactPermissionProviderForMergingJsonWithArtifact( + userPermissionGroups)); + }) + .flatMap(tuple2 -> { + Set 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 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 importArtifactInWorkspace( + String workspaceId, + ArtifactExchangeJson artifactExchangeJson, + String artifactId, + String branchName, + boolean appendToArtifact, + ImportArtifactPermissionProvider permissionProvider, + Set 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 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 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 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 importedArtifactMono = workspaceMono + .then(Mono.defer(() -> artifactSpecificImportableEntities)) + .then(Mono.defer(() -> contextBasedImportService.updateAndSaveArtifactInContext( + importedDoc.getImportableArtifact(), + importingMetaDTO, + mappedImportableResourcesDTO, + currUserMono))) + .cache(); + + Mono 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 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 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 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 generateImportableEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono importedArtifactMono, + ArtifactExchangeJson artifactExchangeJson) { + + ContextBasedImportService contextBasedImportService = + getContextBasedImportService(artifactExchangeJson); + + Flux artifactAgnosticImportables = generateArtifactIndependentImportableEntities( + importingMetaDTO, + mappedImportableResourcesDTO, + workspaceMono, + importedArtifactMono, + artifactExchangeJson); + + Flux artifactSpecificImportables = + contextBasedImportService.generateArtifactContextIndependentImportableEntities( + importingMetaDTO, + mappedImportableResourcesDTO, + workspaceMono, + importedArtifactMono, + artifactExchangeJson); + + Flux 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 generateArtifactIndependentImportableEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono importedArtifactMono, + ArtifactExchangeJson artifactExchangeJson) { + + // Updates plugin map in importable resources + Mono 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 importedDatasourcesMono = installedPluginsMono.then(datasourceImportableService.importEntities( + importingMetaDTO, + mappedImportableResourcesDTO, + workspaceMono, + importedArtifactMono, + artifactExchangeJson, + false, + true)); + + // Directly updates required theme information in DB + Mono 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 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 eventData = + Map.of(artifactContextString, importableArtifact, FieldName.WORKSPACE, workspace); + + final Map 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 sendImportRelatedAnalyticsEvent( + ArtifactExchangeJson artifactExchangeJson, + ImportableArtifact importableArtifact, + Stopwatch stopwatch, + User currentUser) { + + Map 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); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceImpl.java new file mode 100644 index 0000000000..058c871d10 --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/ImportServiceImpl.java @@ -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 customJSLibImportableService, + PermissionGroupRepository permissionGroupRepository, + TransactionalOperator transactionalOperator, + AnalyticsService analyticsService, + ImportableService pluginImportableService, + ImportableService datasourceImportableService, + ImportableService themeImportableService) { + super( + applicationImportService, + sessionUserService, + workspaceService, + customJSLibImportableService, + permissionGroupRepository, + transactionalOperator, + analyticsService, + pluginImportableService, + datasourceImportableService, + themeImportableService); + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/PartialImportServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/PartialImportServiceCEImpl.java index a5a45caa31..2b1ffb1e10 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/PartialImportServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/imports/internal/PartialImportServiceCEImpl.java @@ -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 getImportApplicationPermissions() { + private Mono 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 pageNameMap = new HashMap<>(); pageNameMap.put(pageName, newPage); - mappedImportableResourcesDTO.setPageNameMap(pageNameMap); + mappedImportableResourcesDTO.setPageOrModuleMap(pageNameMap); if (applicationJson.getActionList() == null) { return Mono.just(pageName); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/ArtifactSchemaMigration.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/ArtifactSchemaMigration.java new file mode 100644 index 0000000000..d5e9cf642e --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/ArtifactSchemaMigration.java @@ -0,0 +1,3 @@ +package com.appsmith.server.migrations; + +public class ArtifactSchemaMigration extends ArtifactSchemaMigrationCE {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/ArtifactSchemaMigrationCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/ArtifactSchemaMigrationCE.java new file mode 100644 index 0000000000..02ab8fb0fc --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/migrations/ArtifactSchemaMigrationCE.java @@ -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; + } +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/imports/NewActionImportableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/imports/NewActionImportableServiceCEImpl.java index aec0187de2..1239044154 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/imports/NewActionImportableServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/newactions/imports/NewActionImportableServiceCEImpl.java @@ -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 importedNewActionList = applicationJson.getActionList(); Mono> importedNewActionMono = Mono.justOrEmpty(importedNewActionList); - if (TRUE.equals(importingMetaDTO.getAppendToApp())) { + if (TRUE.equals(importingMetaDTO.getAppendToArtifact())) { importedNewActionMono = importedNewActionMono.map(importedNewActionList1 -> { - List importedNewPages = mappedImportableResourcesDTO.getPageNameMap().values().stream() + List importedNewPages = mappedImportableResourcesDTO.getPageOrModuleMap().values().stream() .distinct() + .map(branchAwareDomain -> (NewPage) branchAwareDomain) .toList(); - Map newToOldNameMap = mappedImportableResourcesDTO.getNewPageNameToOldPageNameMap(); + Map newToOldNameMap = mappedImportableResourcesDTO.getPageOrModuleNewNameToOldName(); for (NewPage newPage : importedNewPages) { String newPageName = newPage.getUnpublishedPage().getName(); @@ -98,8 +99,8 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE invalidActionIds = new HashSet<>(); @@ -159,8 +160,8 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE invalidCollectionIds = new HashSet<>(); @@ -273,7 +274,8 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE) + mappedImportableResourcesDTO.getPageOrModuleMap(), importActionResultDTO.getActionIdMap()); sanitizeDatasourceInActionDTO( unpublishedAction, @@ -290,7 +292,8 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE) + mappedImportableResourcesDTO.getPageOrModuleMap(), importActionResultDTO.getActionIdMap()); parentPage = parentPage == null ? publishedActionPage : parentPage; sanitizeDatasourceInActionDTO( @@ -484,7 +487,7 @@ public class NewActionImportableServiceCEImpl implements ImportableServiceCE newPages = mappedImportableResourcesDTO.getPageNameMap().values().stream() + List 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 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, Map>> importedNewPagesMono, MappedImportableResourcesDTO mappedImportableResourcesDTO) { - List editModeApplicationPages = importedApplication.getPages(); - List publishedModeApplicationPages = importedApplication.getPublishedPages(); + // The access source has been changes because the order of execution has changed. + List editModeApplicationPages = (List) 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 publishedModeApplicationPages = (List) 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> unpublishedPagesMono = importUnpublishedPages(editModeApplicationPages, appendToApp, applicationMono, importedNewPagesMono); @@ -234,7 +251,7 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE pageNameMap = objects.getT3(); Application savedApp = objects.getT4(); - mappedImportableResourcesDTO.setPageNameMap(pageNameMap); + mappedImportableResourcesDTO.setPageOrModuleMap(pageNameMap); log.debug("New pages imported for application: {}", savedApp.getId()); Map> applicationPages = new HashMap<>(); @@ -370,7 +387,7 @@ public class NewPageImportableServiceCEImpl implements ImportableServiceCE> existingPages, - ImportApplicationPermissionProvider permissionProvider) { + ImportArtifactPermissionProvider permissionProvider) { Map oldToNewLayoutIds = new HashMap<>(); pages.forEach(newPage -> { diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/plugins/imports/PluginImportableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/plugins/imports/PluginImportableServiceCEImpl.java index c824ce1df2..c7697f4bfd 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/plugins/imports/PluginImportableServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/plugins/imports/PluginImportableServiceCEImpl.java @@ -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 log.debug("time to get plugin map: {}", tuples.getT1())) .then(); } + + @Override + public Mono importEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono 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); + }); + } } diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ArtifactPermission.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ArtifactPermission.java new file mode 100644 index 0000000000..d6cc0b1dec --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ArtifactPermission.java @@ -0,0 +1,5 @@ +package com.appsmith.server.solutions; + +import com.appsmith.server.solutions.ce.ArtifactPermissionCE; + +public interface ArtifactPermission extends ArtifactPermissionCE {} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ApplicationPermissionCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ApplicationPermissionCE.java index 79e6328a2f..1bf23e8d27 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ApplicationPermissionCE.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ApplicationPermissionCE.java @@ -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(); diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ArtifactPermissionCE.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ArtifactPermissionCE.java new file mode 100644 index 0000000000..f2e9b574bb --- /dev/null +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/solutions/ce/ArtifactPermissionCE.java @@ -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(); +} diff --git a/app/server/appsmith-server/src/main/java/com/appsmith/server/themes/imports/ThemeImportableServiceCEImpl.java b/app/server/appsmith-server/src/main/java/com/appsmith/server/themes/imports/ThemeImportableServiceCEImpl.java index 40949be952..086f9bc7b7 100644 --- a/app/server/appsmith-server/src/main/java/com/appsmith/server/themes/imports/ThemeImportableServiceCEImpl.java +++ b/app/server/appsmith-server/src/main/java/com/appsmith/server/themes/imports/ThemeImportableServiceCEImpl.java @@ -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 Mono 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 } }); } + + @Override + public Mono importEntities( + ImportingMetaDTO importingMetaDTO, + MappedImportableResourcesDTO mappedImportableResourcesDTO, + Mono workspaceMono, + Mono 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); + }); + } } diff --git a/app/server/appsmith-server/src/main/resources/application.properties b/app/server/appsmith-server/src/main/resources/application.properties index 949752ecc3..b680f8c262 100644 --- a/app/server/appsmith-server/src/main/resources/application.properties +++ b/app/server/appsmith-server/src/main/resources/application.properties @@ -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} diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/controllers/ApplicationControllerTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/controllers/ApplicationControllerTest.java index f7c318b961..b40f704293 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/controllers/ApplicationControllerTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/controllers/ApplicationControllerTest.java @@ -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; diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/ImportApplicationPermissionProviderTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/ImportArtifactPermissionProviderTest.java similarity index 85% rename from app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/ImportApplicationPermissionProviderTest.java rename to app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/ImportArtifactPermissionProviderTest.java index 1930362aaa..3bf73e03f7 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/ImportApplicationPermissionProviderTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/helpers/ce/ImportArtifactPermissionProviderTest.java @@ -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 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 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, diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java new file mode 100644 index 0000000000..c2c64b193c --- /dev/null +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/imports/internal/ImportServiceTests.java @@ -0,0 +1,5181 @@ +package com.appsmith.server.imports.internal; + +import com.appsmith.external.helpers.AppsmithBeanUtils; +import com.appsmith.external.models.ActionConfiguration; +import com.appsmith.external.models.ActionDTO; +import com.appsmith.external.models.BearerTokenAuth; +import com.appsmith.external.models.Connection; +import com.appsmith.external.models.CreatorContextType; +import com.appsmith.external.models.DBAuth; +import com.appsmith.external.models.Datasource; +import com.appsmith.external.models.DatasourceConfiguration; +import com.appsmith.external.models.DatasourceStorage; +import com.appsmith.external.models.DatasourceStorageDTO; +import com.appsmith.external.models.DecryptedSensitiveFields; +import com.appsmith.external.models.InvisibleActionFields; +import com.appsmith.external.models.PluginType; +import com.appsmith.external.models.Policy; +import com.appsmith.external.models.Property; +import com.appsmith.external.models.SSLDetails; +import com.appsmith.server.actioncollections.base.ActionCollectionService; +import com.appsmith.server.applications.base.ApplicationService; +import com.appsmith.server.constants.ArtifactJsonType; +import com.appsmith.server.constants.FieldName; +import com.appsmith.server.constants.SerialiseApplicationObjective; +import com.appsmith.server.datasources.base.DatasourceService; +import com.appsmith.server.domains.ActionCollection; +import com.appsmith.server.domains.Application; +import com.appsmith.server.domains.ApplicationDetail; +import com.appsmith.server.domains.ApplicationMode; +import com.appsmith.server.domains.ApplicationPage; +import com.appsmith.server.domains.CustomJSLib; +import com.appsmith.server.domains.GitApplicationMetadata; +import com.appsmith.server.domains.Layout; +import com.appsmith.server.domains.NewAction; +import com.appsmith.server.domains.NewPage; +import com.appsmith.server.domains.PermissionGroup; +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.ActionCollectionDTO; +import com.appsmith.server.dtos.ApplicationAccessDTO; +import com.appsmith.server.dtos.ApplicationImportDTO; +import com.appsmith.server.dtos.ApplicationJson; +import com.appsmith.server.dtos.ApplicationPagesDTO; +import com.appsmith.server.dtos.ImportableArtifactDTO; +import com.appsmith.server.dtos.PageDTO; +import com.appsmith.server.dtos.PageNameIdDTO; +import com.appsmith.server.exceptions.AppsmithError; +import com.appsmith.server.exceptions.AppsmithException; +import com.appsmith.server.exports.internal.ExportApplicationService; +import com.appsmith.server.helpers.MockPluginExecutor; +import com.appsmith.server.helpers.PluginExecutorHelper; +import com.appsmith.server.imports.importable.ImportService; +import com.appsmith.server.jslibs.base.CustomJSLibService; +import com.appsmith.server.layouts.UpdateLayoutService; +import com.appsmith.server.migrations.ApplicationVersion; +import com.appsmith.server.migrations.JsonSchemaMigration; +import com.appsmith.server.migrations.JsonSchemaVersions; +import com.appsmith.server.newactions.base.NewActionService; +import com.appsmith.server.newpages.base.NewPageService; +import com.appsmith.server.plugins.base.PluginService; +import com.appsmith.server.repositories.ApplicationRepository; +import com.appsmith.server.repositories.CacheableRepositoryHelper; +import com.appsmith.server.repositories.PermissionGroupRepository; +import com.appsmith.server.repositories.PluginRepository; +import com.appsmith.server.repositories.ThemeRepository; +import com.appsmith.server.services.ApplicationPageService; +import com.appsmith.server.services.LayoutActionService; +import com.appsmith.server.services.LayoutCollectionService; +import com.appsmith.server.services.PermissionGroupService; +import com.appsmith.server.services.SessionUserService; +import com.appsmith.server.services.WorkspaceService; +import com.appsmith.server.solutions.ApplicationPermission; +import com.appsmith.server.solutions.EnvironmentPermission; +import com.appsmith.server.solutions.PagePermission; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import lombok.extern.slf4j.Slf4j; +import net.minidev.json.JSONArray; +import net.minidev.json.JSONObject; +import org.apache.commons.lang.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.core.io.buffer.DefaultDataBufferFactory; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.codec.multipart.FilePart; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.LinkedMultiValueMap; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.util.function.Tuple3; +import reactor.util.function.Tuple4; + +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static com.appsmith.external.constants.ce.GitConstantsCE.NAME_SEPARATOR; +import static com.appsmith.server.acl.AclPermission.MANAGE_ACTIONS; +import static com.appsmith.server.acl.AclPermission.MANAGE_APPLICATIONS; +import static com.appsmith.server.acl.AclPermission.MANAGE_DATASOURCES; +import static com.appsmith.server.acl.AclPermission.MANAGE_PAGES; +import static com.appsmith.server.acl.AclPermission.READ_ACTIONS; +import static com.appsmith.server.acl.AclPermission.READ_APPLICATIONS; +import static com.appsmith.server.acl.AclPermission.READ_PAGES; +import static com.appsmith.server.acl.AclPermission.READ_WORKSPACES; +import static com.appsmith.server.constants.ce.FieldNameCE.DEFAULT_PAGE_LAYOUT; +import static com.appsmith.server.dtos.ce.CustomJSLibContextCE_DTO.getDTOFromCustomJSLib; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +@Slf4j +@ExtendWith(SpringExtension.class) +@SpringBootTest +@DirtiesContext +@TestMethodOrder(MethodOrderer.MethodName.class) +public class ImportServiceTests { + + private static final String INVALID_JSON_FILE = "invalid json file"; + private static final Map datasourceMap = new HashMap<>(); + private static Plugin installedPlugin; + private static String workspaceId; + private static String defaultEnvironmentId; + private static String testAppId; + private static Datasource jsDatasource; + private static Plugin installedJsPlugin; + private static Boolean isSetupDone = false; + private static String exportWithConfigurationAppId; + + @Autowired + ImportService importService; + + @Autowired + ExportApplicationService exportApplicationService; + + // @Autowired + // ImportApplicationService importApplicationService; + + @Autowired + Gson gson; + + @Autowired + ApplicationPageService applicationPageService; + + @Autowired + PluginRepository pluginRepository; + + @Autowired + ApplicationRepository applicationRepository; + + @Autowired + DatasourceService datasourceService; + + @Autowired + NewPageService newPageService; + + @Autowired + NewActionService newActionService; + + @Autowired + WorkspaceService workspaceService; + + @Autowired + LayoutActionService layoutActionService; + + @Autowired + UpdateLayoutService updateLayoutService; + + @Autowired + LayoutCollectionService layoutCollectionService; + + @Autowired + ActionCollectionService actionCollectionService; + + @MockBean + PluginExecutorHelper pluginExecutorHelper; + + @Autowired + ThemeRepository themeRepository; + + @Autowired + ApplicationService applicationService; + + @Autowired + PermissionGroupRepository permissionGroupRepository; + + @Autowired + PermissionGroupService permissionGroupService; + + @Autowired + CustomJSLibService customJSLibService; + + @Autowired + EnvironmentPermission environmentPermission; + + @Autowired + PagePermission pagePermission; + + @Autowired + ApplicationPermission applicationPermission; + + @SpyBean + PluginService pluginService; + + @Autowired + CacheableRepositoryHelper cacheableRepositoryHelper; + + @Autowired + SessionUserService sessionUserService; + + @BeforeEach + public void setup() { + Mockito.when(pluginExecutorHelper.getPluginExecutor(Mockito.any())) + .thenReturn(Mono.just(new MockPluginExecutor())); + + if (Boolean.TRUE.equals(isSetupDone)) { + return; + } + User currentUser = sessionUserService.getCurrentUser().block(); + Set beforeCreatingWorkspace = + cacheableRepositoryHelper.getPermissionGroupsOfUser(currentUser).block(); + log.info("Permission Groups for User before creating workspace: {}", beforeCreatingWorkspace); + installedPlugin = pluginRepository.findByPackageName("installed-plugin").block(); + Workspace workspace = new Workspace(); + workspace.setName("Import-Export-Test-Workspace"); + Workspace savedWorkspace = workspaceService.create(workspace).block(); + workspaceId = savedWorkspace.getId(); + defaultEnvironmentId = workspaceService + .getDefaultEnvironmentId(workspaceId, environmentPermission.getExecutePermission()) + .block(); + Set afterCreatingWorkspace = + cacheableRepositoryHelper.getPermissionGroupsOfUser(currentUser).block(); + log.info("Permission Groups for User after creating workspace: {}", afterCreatingWorkspace); + + log.info("Workspace ID: {}", workspaceId); + log.info("Workspace Role Ids: {}", workspace.getDefaultPermissionGroups()); + log.info("Policy for created Workspace: {}", workspace.getPolicies()); + log.info("Current User ID: {}", currentUser.getId()); + + Application testApplication = new Application(); + testApplication.setName("Export-Application-Test-Application"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + + Application.ThemeSetting themeSettings = getThemeSetting(); + testApplication.setUnpublishedApplicationDetail(new ApplicationDetail()); + testApplication.getUnpublishedApplicationDetail().setThemeSetting(themeSettings); + + Application savedApplication = applicationPageService + .createApplication(testApplication, workspaceId) + .block(); + testAppId = savedApplication.getId(); + + Datasource ds1 = new Datasource(); + ds1.setName("DS1"); + ds1.setWorkspaceId(workspaceId); + ds1.setPluginId(installedPlugin.getId()); + final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setUrl("http://example.org/get"); + datasourceConfiguration.setHeaders(List.of(new Property("X-Answer", "42"))); + + HashMap storages1 = new HashMap<>(); + storages1.put( + defaultEnvironmentId, new DatasourceStorageDTO(null, defaultEnvironmentId, datasourceConfiguration)); + ds1.setDatasourceStorages(storages1); + + Datasource ds2 = new Datasource(); + ds2.setName("DS2"); + ds2.setPluginId(installedPlugin.getId()); + ds2.setWorkspaceId(workspaceId); + DatasourceConfiguration datasourceConfiguration2 = new DatasourceConfiguration(); + DBAuth auth = new DBAuth(); + auth.setPassword("awesome-password"); + datasourceConfiguration2.setAuthentication(auth); + HashMap storages2 = new HashMap<>(); + storages2.put( + defaultEnvironmentId, new DatasourceStorageDTO(null, defaultEnvironmentId, datasourceConfiguration2)); + ds2.setDatasourceStorages(storages2); + + jsDatasource = new Datasource(); + jsDatasource.setName("Default JS datasource"); + jsDatasource.setWorkspaceId(workspaceId); + installedJsPlugin = + pluginRepository.findByPackageName("installed-js-plugin").block(); + assert installedJsPlugin != null; + jsDatasource.setPluginId(installedJsPlugin.getId()); + + ds1 = datasourceService.create(ds1).block(); + ds2 = datasourceService.create(ds2).block(); + datasourceMap.put("DS1", ds1); + datasourceMap.put("DS2", ds2); + isSetupDone = true; + } + + private Flux getActionsInApplication(Application application) { + return newPageService + // fetch the unpublished pages + .findByApplicationId(application.getId(), READ_PAGES, false) + .flatMap(page -> newActionService.getUnpublishedActions( + new LinkedMultiValueMap<>(Map.of(FieldName.PAGE_ID, Collections.singletonList(page.getId()))), + "")); + } + + private FilePart createFilePart(String filePath) { + FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); + Flux dataBufferFlux = DataBufferUtils.read( + new ClassPathResource(filePath), new DefaultDataBufferFactory(), 4096) + .cache(); + + Mockito.when(filepart.content()).thenReturn(dataBufferFlux); + Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.APPLICATION_JSON); + + return filepart; + } + + private Mono createAppJson(String filePath) { + FilePart filePart = createFilePart(filePath); + + Mono stringifiedFile = DataBufferUtils.join(filePart.content()).map(dataBuffer -> { + byte[] data = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(data); + DataBufferUtils.release(dataBuffer); + return new String(data); + }); + + return stringifiedFile + .map(data -> { + return gson.fromJson(data, ApplicationJson.class); + }) + .map(JsonSchemaMigration::migrateApplicationToLatestSchema); + } + + private Workspace createTemplateWorkspace() { + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + return workspaceService.create(newWorkspace).block(); + } + + @Test + @WithUserDetails(value = "api_user") + public void exportApplicationById_WhenContainsInternalFields_InternalFieldsNotExported() { + Mono resultMono = exportApplicationService.exportApplicationById(testAppId, ""); + + StepVerifier.create(resultMono) + .assertNext(applicationJson -> { + Application exportedApplication = applicationJson.getExportedApplication(); + assertThat(exportedApplication.getModifiedBy()).isNull(); + assertThat(exportedApplication.getLastUpdateTime()).isNull(); + assertThat(exportedApplication.getLastEditedAt()).isNull(); + assertThat(exportedApplication.getLastDeployedAt()).isNull(); + assertThat(exportedApplication.getGitApplicationMetadata()).isNull(); + assertThat(exportedApplication.getEditModeThemeId()).isNull(); + assertThat(exportedApplication.getPublishedModeThemeId()).isNull(); + assertThat(exportedApplication.getExportWithConfiguration()).isNull(); + assertThat(exportedApplication.getForkWithConfiguration()).isNull(); + assertThat(exportedApplication.getForkingEnabled()).isNull(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createExportAppJsonWithDatasourceButWithoutActionsTest() { + + Application testApplication = new Application(); + testApplication.setName("Another Export Application"); + + final Mono resultMono = workspaceService + .getById(workspaceId) + .flatMap(workspace -> { + final Datasource ds1 = datasourceMap.get("DS1"); + ds1.setWorkspaceId(workspace.getId()); + + final Datasource ds2 = datasourceMap.get("DS2"); + ds2.setWorkspaceId(workspace.getId()); + + return applicationPageService.createApplication(testApplication, workspaceId); + }) + .flatMap(application -> exportApplicationService.exportApplicationById(application.getId(), "")); + + StepVerifier.create(resultMono) + .assertNext(applicationJson -> { + assertThat(applicationJson.getPageList()).hasSize(1); + assertThat(applicationJson.getActionList()).isEmpty(); + assertThat(applicationJson.getDatasourceList()).isEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createExportAppJsonWithActionAndActionCollectionTest() { + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("template-org-with-ds"); + + Application testApplication = new Application(); + testApplication.setName("ApplicationWithActionCollectionAndDatasource"); + testApplication = applicationPageService + .createApplication(testApplication, workspaceId) + .block(); + + assert testApplication != null; + final String appName = testApplication.getName(); + final Mono resultMono = Mono.zip( + Mono.just(testApplication), + newPageService.findPageById( + testApplication.getPages().get(0).getId(), READ_PAGES, false)) + .flatMap(tuple -> { + Application testApp = tuple.getT1(); + PageDTO testPage = tuple.getT2(); + + Layout layout = testPage.getLayouts().get(0); + ObjectMapper objectMapper = new ObjectMapper(); + JSONObject dsl = new JSONObject(); + try { + dsl = new JSONObject(objectMapper.readValue( + DEFAULT_PAGE_LAYOUT, new TypeReference>() {})); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + + ArrayList children = (ArrayList) dsl.get("children"); + JSONObject testWidget = new JSONObject(); + testWidget.put("widgetName", "firstWidget"); + JSONArray temp = new JSONArray(); + temp.add(new JSONObject(Map.of("key", "testField"))); + testWidget.put("dynamicBindingPathList", temp); + testWidget.put("testField", "{{ validAction.data }}"); + children.add(testWidget); + + JSONObject tableWidget = new JSONObject(); + tableWidget.put("widgetName", "Table1"); + tableWidget.put("type", "TABLE_WIDGET"); + Map primaryColumns = new HashMap<>(); + JSONObject jsonObject = new JSONObject(Map.of("key", "value")); + primaryColumns.put("_id", "{{ PageAction.data }}"); + primaryColumns.put("_class", jsonObject); + tableWidget.put("primaryColumns", primaryColumns); + final ArrayList objects = new ArrayList<>(); + JSONArray temp2 = new JSONArray(); + temp2.add(new JSONObject(Map.of("key", "primaryColumns._id"))); + tableWidget.put("dynamicBindingPathList", temp2); + children.add(tableWidget); + + layout.setDsl(dsl); + layout.setPublishedDsl(dsl); + + ActionDTO action = new ActionDTO(); + action.setName("validAction"); + action.setPageId(testPage.getId()); + action.setExecuteOnLoad(true); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(datasourceMap.get("DS2")); + + ActionDTO action2 = new ActionDTO(); + action2.setName("validAction2"); + action2.setPageId(testPage.getId()); + action2.setExecuteOnLoad(true); + action2.setUserSetOnLoad(true); + ActionConfiguration actionConfiguration2 = new ActionConfiguration(); + actionConfiguration2.setHttpMethod(HttpMethod.GET); + action2.setActionConfiguration(actionConfiguration2); + action2.setDatasource(datasourceMap.get("DS2")); + + ActionCollectionDTO actionCollectionDTO1 = new ActionCollectionDTO(); + actionCollectionDTO1.setName("testCollection1"); + actionCollectionDTO1.setPageId(testPage.getId()); + actionCollectionDTO1.setApplicationId(testApp.getId()); + actionCollectionDTO1.setWorkspaceId(testApp.getWorkspaceId()); + actionCollectionDTO1.setPluginId(jsDatasource.getPluginId()); + ActionDTO action1 = new ActionDTO(); + action1.setName("testAction1"); + action1.setActionConfiguration(new ActionConfiguration()); + action1.getActionConfiguration().setBody("mockBody"); + actionCollectionDTO1.setActions(List.of(action1)); + actionCollectionDTO1.setPluginType(PluginType.JS); + + return layoutCollectionService + .createCollection(actionCollectionDTO1, null) + .then(layoutActionService.createSingleAction(action, Boolean.FALSE)) + .then(layoutActionService.createSingleAction(action2, Boolean.FALSE)) + .then(updateLayoutService.updateLayout( + testPage.getId(), testPage.getApplicationId(), layout.getId(), layout)) + .then(exportApplicationService.exportApplicationById(testApp.getId(), "")); + }) + .cache(); + + Mono> actionListMono = resultMono.then(newActionService + .findAllByApplicationIdAndViewMode(testApplication.getId(), false, READ_ACTIONS, null) + .collectList()); + + Mono> collectionListMono = resultMono.then(actionCollectionService + .findAllByApplicationIdAndViewMode(testApplication.getId(), false, READ_ACTIONS, null) + .collectList()); + + Mono> pageListMono = resultMono.then(newPageService + .findNewPagesByApplicationId(testApplication.getId(), READ_PAGES) + .collectList()); + + StepVerifier.create(Mono.zip(resultMono, actionListMono, collectionListMono, pageListMono)) + .assertNext(tuple -> { + ApplicationJson applicationJson = tuple.getT1(); + List DBActions = tuple.getT2(); + List DBCollections = tuple.getT3(); + List DBPages = tuple.getT4(); + + Application exportedApp = applicationJson.getExportedApplication(); + List pageList = applicationJson.getPageList(); + List actionList = applicationJson.getActionList(); + List actionCollectionList = applicationJson.getActionCollectionList(); + List datasourceList = applicationJson.getDatasourceList(); + + List exportedCollectionIds = actionCollectionList.stream() + .map(ActionCollection::getId) + .collect(Collectors.toList()); + List exportedActionIds = + actionList.stream().map(NewAction::getId).collect(Collectors.toList()); + List DBCollectionIds = + DBCollections.stream().map(ActionCollection::getId).collect(Collectors.toList()); + List DBActionIds = + DBActions.stream().map(NewAction::getId).collect(Collectors.toList()); + List DBOnLayoutLoadActionIds = new ArrayList<>(); + List exportedOnLayoutLoadActionIds = new ArrayList<>(); + + assertThat(DBPages).hasSize(1); + DBPages.forEach( + newPage -> newPage.getUnpublishedPage().getLayouts().forEach(layout -> { + if (layout.getLayoutOnLoadActions() != null) { + layout.getLayoutOnLoadActions().forEach(dslActionDTOSet -> { + dslActionDTOSet.forEach( + actionDTO -> DBOnLayoutLoadActionIds.add(actionDTO.getId())); + }); + } + })); + pageList.forEach( + newPage -> newPage.getUnpublishedPage().getLayouts().forEach(layout -> { + if (layout.getLayoutOnLoadActions() != null) { + layout.getLayoutOnLoadActions().forEach(dslActionDTOSet -> { + dslActionDTOSet.forEach( + actionDTO -> exportedOnLayoutLoadActionIds.add(actionDTO.getId())); + }); + } + })); + + NewPage defaultPage = pageList.get(0); + + // Check if the mongo escaped widget names are carried to exported file from DB + Layout pageLayout = + DBPages.get(0).getUnpublishedPage().getLayouts().get(0); + Set mongoEscapedWidgets = pageLayout.getMongoEscapedWidgetNames(); + Set expectedMongoEscapedWidgets = Set.of("Table1"); + assertThat(mongoEscapedWidgets).isEqualTo(expectedMongoEscapedWidgets); + + pageLayout = + pageList.get(0).getUnpublishedPage().getLayouts().get(0); + Set exportedMongoEscapedWidgets = pageLayout.getMongoEscapedWidgetNames(); + assertThat(exportedMongoEscapedWidgets).isEqualTo(expectedMongoEscapedWidgets); + + assertThat(exportedApp.getName()).isEqualTo(appName); + assertThat(exportedApp.getWorkspaceId()).isNull(); + assertThat(exportedApp.getPages()).hasSize(1); + assertThat(exportedApp.getPages().get(0).getId()) + .isEqualTo(defaultPage.getUnpublishedPage().getName()); + + assertThat(exportedApp.getPolicies()).isNull(); + + assertThat(pageList).hasSize(1); + assertThat(defaultPage.getApplicationId()).isNull(); + assertThat(defaultPage + .getUnpublishedPage() + .getLayouts() + .get(0) + .getDsl()) + .isNotNull(); + assertThat(defaultPage.getId()).isNull(); + assertThat(defaultPage.getPolicies()).isNull(); + + assertThat(actionList.isEmpty()).isFalse(); + assertThat(actionList).hasSize(3); + NewAction validAction = actionList.stream() + .filter(action -> action.getId().equals("Page1_validAction")) + .findFirst() + .get(); + assertThat(validAction.getApplicationId()).isNull(); + assertThat(validAction.getPluginId()).isEqualTo(installedPlugin.getPackageName()); + assertThat(validAction.getPluginType()).isEqualTo(PluginType.API); + assertThat(validAction.getWorkspaceId()).isNull(); + assertThat(validAction.getPolicies()).isNull(); + assertThat(validAction.getId()).isNotNull(); + ActionDTO unpublishedAction = validAction.getUnpublishedAction(); + assertThat(unpublishedAction.getPageId()) + .isEqualTo(defaultPage.getUnpublishedPage().getName()); + assertThat(unpublishedAction.getDatasource().getPluginId()) + .isEqualTo(installedPlugin.getPackageName()); + + NewAction testAction1 = actionList.stream() + .filter(action -> + action.getUnpublishedAction().getName().equals("testAction1")) + .findFirst() + .get(); + assertThat(testAction1.getId()).isEqualTo("Page1_testCollection1.testAction1"); + + assertThat(actionCollectionList.isEmpty()).isFalse(); + assertThat(actionCollectionList).hasSize(1); + final ActionCollection actionCollection = actionCollectionList.get(0); + assertThat(actionCollection.getApplicationId()).isNull(); + assertThat(actionCollection.getWorkspaceId()).isNull(); + assertThat(actionCollection.getPolicies()).isNull(); + assertThat(actionCollection.getId()).isNotNull(); + assertThat(actionCollection.getUnpublishedCollection().getPluginType()) + .isEqualTo(PluginType.JS); + assertThat(actionCollection.getUnpublishedCollection().getPageId()) + .isEqualTo(defaultPage.getUnpublishedPage().getName()); + assertThat(actionCollection.getUnpublishedCollection().getPluginId()) + .isEqualTo(installedJsPlugin.getPackageName()); + + assertThat(datasourceList).hasSize(1); + DatasourceStorage datasource = datasourceList.get(0); + assertThat(datasource.getWorkspaceId()).isNull(); + assertThat(datasource.getId()).isNull(); + assertThat(datasource.getPluginId()).isEqualTo(installedPlugin.getPackageName()); + assertThat(datasource.getDatasourceConfiguration()).isNotNull(); + assertThat(datasource.getDatasourceConfiguration().getAuthentication()) + .isNull(); + assertThat(datasource.getDatasourceConfiguration().getSshProxy()) + .isNull(); + assertThat(datasource.getDatasourceConfiguration().getSshProxyEnabled()) + .isNull(); + assertThat(datasource.getDatasourceConfiguration().getProperties()) + .isNull(); + + assertThat(applicationJson.getInvisibleActionFields()).isNull(); + NewAction validAction2 = actionList.stream() + .filter(action -> action.getId().equals("Page1_validAction2")) + .findFirst() + .get(); + assertEquals(true, validAction2.getUnpublishedAction().getUserSetOnLoad()); + + assertThat(applicationJson.getUnpublishedLayoutmongoEscapedWidgets()) + .isNull(); + assertThat(applicationJson.getPublishedLayoutmongoEscapedWidgets()) + .isNull(); + assertThat(applicationJson.getEditModeTheme()).isNotNull(); + assertThat(applicationJson.getEditModeTheme().isSystemTheme()) + .isTrue(); + assertThat(applicationJson.getEditModeTheme().getName()) + .isEqualToIgnoringCase(Theme.DEFAULT_THEME_NAME); + + assertThat(applicationJson.getPublishedTheme()).isNotNull(); + assertThat(applicationJson.getPublishedTheme().isSystemTheme()) + .isTrue(); + assertThat(applicationJson.getPublishedTheme().getName()) + .isEqualToIgnoringCase(Theme.DEFAULT_THEME_NAME); + + assertThat(exportedCollectionIds).isNotEmpty(); + assertThat(exportedCollectionIds).doesNotContain(String.valueOf(DBCollectionIds)); + + assertThat(exportedActionIds).isNotEmpty(); + assertThat(exportedActionIds).doesNotContain(String.valueOf(DBActionIds)); + + assertThat(exportedOnLayoutLoadActionIds).isNotEmpty(); + assertThat(exportedOnLayoutLoadActionIds).doesNotContain(String.valueOf(DBOnLayoutLoadActionIds)); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createExportAppJsonForGitTest() { + + StringBuilder pageName = new StringBuilder(); + final Mono resultMono = applicationRepository + .findById(testAppId) + .flatMap(testApp -> { + final String pageId = testApp.getPages().get(0).getId(); + return Mono.zip(Mono.just(testApp), newPageService.findPageById(pageId, READ_PAGES, false)); + }) + .flatMap(tuple -> { + Datasource ds1 = datasourceMap.get("DS1"); + Application testApp = tuple.getT1(); + PageDTO testPage = tuple.getT2(); + pageName.append(testPage.getName()); + + Layout layout = testPage.getLayouts().get(0); + JSONObject dsl = new JSONObject(Map.of("text", "{{ query1.data }}")); + + layout.setDsl(dsl); + layout.setPublishedDsl(dsl); + + ActionDTO action = new ActionDTO(); + action.setName("validAction"); + action.setPageId(testPage.getId()); + action.setExecuteOnLoad(true); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(ds1); + + return layoutActionService + .createAction(action) + .then(exportApplicationService.exportApplicationById( + testApp.getId(), SerialiseApplicationObjective.VERSION_CONTROL)); + }); + + StepVerifier.create(resultMono) + .assertNext(applicationJson -> { + Application exportedApp = applicationJson.getExportedApplication(); + List pageList = applicationJson.getPageList(); + List actionList = applicationJson.getActionList(); + List datasourceList = applicationJson.getDatasourceList(); + + NewPage newPage = pageList.get(0); + + assertThat(applicationJson.getServerSchemaVersion()).isEqualTo(JsonSchemaVersions.serverVersion); + assertThat(applicationJson.getClientSchemaVersion()).isEqualTo(JsonSchemaVersions.clientVersion); + + assertThat(exportedApp.getName()).isNotNull(); + assertThat(exportedApp.getWorkspaceId()).isNull(); + assertThat(exportedApp.getPages()).hasSize(1); + assertThat(exportedApp.getPages().get(0).getId()).isEqualTo(pageName.toString()); + assertThat(exportedApp.getGitApplicationMetadata()).isNull(); + + assertThat(exportedApp.getApplicationDetail()).isNotNull(); + assertThat(exportedApp.getApplicationDetail().getThemeSetting()) + .isNotNull(); + assertThat(exportedApp + .getApplicationDetail() + .getThemeSetting() + .getSizing()) + .isNotNull(); + assertThat(exportedApp + .getApplicationDetail() + .getThemeSetting() + .getAccentColor()) + .isEqualTo("#FFFFFF"); + assertThat(exportedApp + .getApplicationDetail() + .getThemeSetting() + .getColorMode()) + .isEqualTo(Application.ThemeSetting.Type.LIGHT); + assertThat(exportedApp + .getApplicationDetail() + .getThemeSetting() + .getDensity()) + .isEqualTo(1); + assertThat(exportedApp + .getApplicationDetail() + .getThemeSetting() + .getFontFamily()) + .isEqualTo("#000000"); + assertThat(exportedApp + .getApplicationDetail() + .getThemeSetting() + .getSizing()) + .isEqualTo(1); + + assertThat(exportedApp.getPolicies()).isNull(); + assertThat(exportedApp.getUserPermissions()).isNull(); + + assertThat(pageList).hasSize(1); + assertThat(newPage.getApplicationId()).isNull(); + assertThat(newPage.getUnpublishedPage().getLayouts().get(0).getDsl()) + .isNotNull(); + assertThat(newPage.getId()).isNull(); + assertThat(newPage.getPolicies()).isNull(); + + assertThat(actionList.isEmpty()).isFalse(); + NewAction validAction = actionList.get(0); + assertThat(validAction.getApplicationId()).isNull(); + assertThat(validAction.getPluginId()).isEqualTo(installedPlugin.getPackageName()); + assertThat(validAction.getPluginType()).isEqualTo(PluginType.API); + assertThat(validAction.getWorkspaceId()).isNull(); + assertThat(validAction.getPolicies()).isNull(); + assertThat(validAction.getId()).isNotNull(); + assertThat(validAction.getUnpublishedAction().getPageId()) + .isEqualTo(newPage.getUnpublishedPage().getName()); + + assertThat(datasourceList).hasSize(1); + DatasourceStorage datasource = datasourceList.get(0); + assertThat(datasource.getWorkspaceId()).isNull(); + assertThat(datasource.getId()).isNull(); + assertThat(datasource.getPluginId()).isEqualTo(installedPlugin.getPackageName()); + assertThat(datasource.getDatasourceConfiguration()).isNull(); + assertThat(applicationJson.getUnpublishedLayoutmongoEscapedWidgets()) + .isNull(); + assertThat(applicationJson.getPublishedLayoutmongoEscapedWidgets()) + .isNull(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifactFromInvalidFileTest() { + FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); + Flux dataBufferFlux = DataBufferUtils.read( + new ClassPathResource("test_assets/WorkspaceServiceTest/my_workspace_logo.png"), + new DefaultDataBufferFactory(), + 4096) + .cache(); + + Mockito.when(filepart.content()).thenReturn(dataBufferFlux); + Mockito.when(filepart.headers().getContentType()).thenReturn(MediaType.IMAGE_PNG); + + Mono resultMono = importService.extractArtifactExchangeJsonAndSaveArtifact( + filepart, workspaceId, null, ArtifactJsonType.APPLICATION); + + StepVerifier.create(resultMono) + .expectErrorMatches(error -> error instanceof AppsmithException) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifactWithNullWorkspaceIdTest() { + FilePart filepart = Mockito.mock(FilePart.class, Mockito.RETURNS_DEEP_STUBS); + + Mono resultMono = importService.extractArtifactExchangeJsonAndSaveArtifact( + filepart, null, null, ArtifactJsonType.APPLICATION); + + StepVerifier.create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException + && throwable + .getMessage() + .equals(AppsmithError.INVALID_PARAMETER.getMessage(FieldName.WORKSPACE_ID))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifactFromInvalidJsonFileWithoutPagesTest() { + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/invalid-json-without-pages.json"); + + Mono resultMono = importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspaceId, null, ArtifactJsonType.APPLICATION); + + StepVerifier.create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException + && throwable + .getMessage() + .equals(AppsmithError.VALIDATION_FAILURE.getMessage( + "Field '" + FieldName.PAGE_LIST + "' is missing in the JSON."))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifactFromInvalidJsonFileWithoutArtifactTest() { + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/invalid-json-without-app.json"); + Mono resultMono = importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspaceId, null, ArtifactJsonType.APPLICATION); + + StepVerifier.create(resultMono) + .expectErrorMatches( + throwable -> throwable instanceof AppsmithException + && throwable + .getMessage() + .equals( + AppsmithError.VALIDATION_FAILURE.getMessage( + "Field '" + FieldName.APPLICATION + + "' Sorry! Seems like you've imported a page-level json instead of an application. Please use the import within the page."))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifactFromValidJsonFileTest() { + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + + Mono workspaceMono = workspaceService.create(newWorkspace).cache(); + + String environmentId = workspaceMono + .flatMap(workspace -> workspaceService.getDefaultEnvironmentId( + workspace.getId(), environmentPermission.getExecutePermission())) + .block(); + + final Mono resultMono = + workspaceMono.flatMap(workspace -> importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspace.getId(), null, ArtifactJsonType.APPLICATION)); + + List permissionGroups = workspaceMono + .flatMapMany(savedWorkspace -> { + Set defaultPermissionGroups = savedWorkspace.getDefaultPermissionGroups(); + return permissionGroupRepository.findAllById(defaultPermissionGroups); + }) + .collectList() + .block(); + + PermissionGroup adminPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.ADMINISTRATOR)) + .findFirst() + .get(); + + PermissionGroup developerPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.DEVELOPER)) + .findFirst() + .get(); + + PermissionGroup viewerPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.VIEWER)) + .findFirst() + .get(); + + Policy manageAppPolicy = Policy.builder() + .permission(MANAGE_APPLICATIONS.getValue()) + .permissionGroups(Set.of(adminPermissionGroup.getId(), developerPermissionGroup.getId())) + .build(); + Policy readAppPolicy = Policy.builder() + .permission(READ_APPLICATIONS.getValue()) + .permissionGroups(Set.of( + adminPermissionGroup.getId(), developerPermissionGroup.getId(), viewerPermissionGroup.getId())) + .build(); + + StepVerifier.create(resultMono.flatMap(importArtifactDTO -> { + ApplicationImportDTO applicationImportDTO = (ApplicationImportDTO) importArtifactDTO; + Application application = applicationImportDTO.getApplication(); + return Mono.zip( + Mono.just(applicationImportDTO), + datasourceService + .getAllByWorkspaceIdWithStorages( + application.getWorkspaceId(), Optional.of(MANAGE_DATASOURCES)) + .collectList(), + newActionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList(), + newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList(), + actionCollectionService + .findAllByApplicationIdAndViewMode(application.getId(), false, MANAGE_ACTIONS, null) + .collectList(), + customJSLibService.getAllJSLibsInContext( + application.getId(), CreatorContextType.APPLICATION, null, false)); + })) + .assertNext(tuple -> { + final Application application = tuple.getT1().getApplication(); + final List unConfiguredDatasourceList = + tuple.getT1().getUnConfiguredDatasourceList(); + final boolean isPartialImport = tuple.getT1().getIsPartialImport(); + final List datasourceList = tuple.getT2(); + final List actionList = tuple.getT3(); + final List pageList = tuple.getT4(); + final List actionCollectionList = tuple.getT5(); + final List importedJSLibList = tuple.getT6(); + + // although the imported list had only one jsLib entry, the other entry comes from ensuring an xml + // parser entry + // for backward compatibility + assertEquals(2, importedJSLibList.size()); + CustomJSLib importedJSLib = (CustomJSLib) importedJSLibList.toArray()[0]; + CustomJSLib expectedJSLib = new CustomJSLib( + "TestLib", Set.of("accessor1"), "url", "docsUrl", "1" + ".0", "defs_string"); + assertEquals(expectedJSLib.getName(), importedJSLib.getName()); + assertEquals(expectedJSLib.getAccessor(), importedJSLib.getAccessor()); + assertEquals(expectedJSLib.getUrl(), importedJSLib.getUrl()); + assertEquals(expectedJSLib.getDocsUrl(), importedJSLib.getDocsUrl()); + assertEquals(expectedJSLib.getVersion(), importedJSLib.getVersion()); + assertEquals(expectedJSLib.getDefs(), importedJSLib.getDefs()); + // although the imported list had only one jsLib entry, the other entry comes from ensuring an xml + // parser entry + // for backward compatibility + assertEquals(2, application.getUnpublishedCustomJSLibs().size()); + + assertThat(application.getName()).isEqualTo("valid_application"); + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getPages()).hasSize(2); + assertThat(application.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy)); + assertThat(application.getPublishedPages()).hasSize(1); + assertThat(application.getModifiedBy()).isEqualTo("api_user"); + assertThat(application.getUpdatedAt()).isNotNull(); + assertThat(application.getEditModeThemeId()).isNotNull(); + assertThat(application.getPublishedModeThemeId()).isNotNull(); + assertThat(isPartialImport).isEqualTo(Boolean.TRUE); + assertThat(unConfiguredDatasourceList).isNotNull(); + + assertThat(datasourceList).isNotEmpty(); + datasourceList.forEach(datasource -> { + assertThat(datasource.getWorkspaceId()).isEqualTo(application.getWorkspaceId()); + DatasourceStorageDTO storageDTO = + datasource.getDatasourceStorages().get(environmentId); + assertThat(storageDTO.getDatasourceConfiguration()).isNotNull(); + }); + + List collectionIdInAction = new ArrayList<>(); + assertThat(actionList).isNotEmpty(); + actionList.forEach(newAction -> { + ActionDTO actionDTO = newAction.getUnpublishedAction(); + assertThat(actionDTO.getPageId()) + .isNotEqualTo(pageList.get(0).getName()); + + if (StringUtils.equals(actionDTO.getName(), "api_wo_auth")) { + ActionDTO publishedAction = newAction.getPublishedAction(); + assertThat(publishedAction).isNotNull(); + assertThat(publishedAction.getActionConfiguration()).isNotNull(); + // Test the fallback page ID from the unpublishedAction is copied to published version when + // published version does not have pageId + assertThat(actionDTO.getPageId()).isEqualTo(publishedAction.getPageId()); + // check that createAt field is getting populated from JSON + assertThat(actionDTO.getCreatedAt()).isEqualTo("2023-12-13T12:10:02Z"); + } + + if (!StringUtils.isEmpty(actionDTO.getCollectionId())) { + collectionIdInAction.add(actionDTO.getCollectionId()); + assertThat(actionDTO.getDefaultResources().getCollectionId()) + .isEqualTo(actionDTO.getCollectionId()); + } + }); + + assertThat(actionCollectionList).isNotEmpty(); + actionCollectionList.forEach(actionCollection -> { + assertThat(actionCollection.getUnpublishedCollection().getPageId()) + .isNotEqualTo(pageList.get(0).getName()); + if (StringUtils.equals( + actionCollection.getUnpublishedCollection().getName(), "JSObject2")) { + // Check if this action collection is not attached to any action + assertThat(collectionIdInAction).doesNotContain(actionCollection.getId()); + } else { + assertThat(collectionIdInAction).contains(actionCollection.getId()); + } + }); + + assertThat(pageList).hasSize(2); + + ApplicationPage defaultAppPage = application.getPages().stream() + .filter(ApplicationPage::getIsDefault) + .findFirst() + .orElse(null); + assertThat(defaultAppPage).isNotNull(); + + PageDTO defaultPageDTO = pageList.stream() + .filter(pageDTO -> pageDTO.getId().equals(defaultAppPage.getId())) + .findFirst() + .orElse(null); + + assertThat(defaultPageDTO).isNotNull(); + assertThat(defaultPageDTO.getLayouts().get(0).getLayoutOnLoadActions()) + .isNotEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importFromValidJson_cancelledMidway_importSuccess() { + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Midway cancel import app workspace"); + newWorkspace = workspaceService.create(newWorkspace).block(); + + importService + .extractArtifactExchangeJsonAndSaveArtifact( + filePart, newWorkspace.getId(), null, ArtifactJsonType.APPLICATION) + .timeout(Duration.ofMillis(10)) + .subscribe(); + + // Wait for import to complete + Mono importedAppFromDbMono = Mono.just(newWorkspace).flatMap(workspace -> { + try { + // Before fetching the imported application, sleep for 5 seconds to ensure that the import completes + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + return applicationRepository + .findByWorkspaceId(workspace.getId(), READ_APPLICATIONS) + .next(); + }); + + StepVerifier.create(importedAppFromDbMono) + .assertNext(application -> { + assertThat(application.getId()).isNotEmpty(); + }) + .verifyComplete(); + } + + @Test + @Disabled + @WithUserDetails(value = "api_user") + public void importApplicationInWorkspace_WhenCustomizedThemes_ThemesCreated() { + FilePart filePart = + createFilePart("test_assets/ImportExportServiceTest/valid-application-with-custom-themes.json"); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Import theme test org"); + + final Mono resultMono = workspaceService + .create(newWorkspace) + .flatMap(workspace -> importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspace.getId(), null, ArtifactJsonType.APPLICATION)) + .map(artifactImportDTO -> (ApplicationImportDTO) artifactImportDTO); + + StepVerifier.create(resultMono.flatMap(applicationImportDTO -> Mono.zip( + Mono.just(applicationImportDTO), + themeRepository.findById( + applicationImportDTO.getApplication().getEditModeThemeId()), + themeRepository.findById( + applicationImportDTO.getApplication().getPublishedModeThemeId())))) + .assertNext(tuple -> { + final Application application = tuple.getT1().getApplication(); + Theme editTheme = tuple.getT2(); + Theme publishedTheme = tuple.getT3(); + + assertThat(editTheme.isSystemTheme()).isFalse(); + assertThat(editTheme.getDisplayName()).isEqualTo("Custom edit theme"); + assertThat(editTheme.getWorkspaceId()).isNull(); + assertThat(editTheme.getApplicationId()).isNull(); + + assertThat(publishedTheme.isSystemTheme()).isFalse(); + assertThat(publishedTheme.getDisplayName()).isEqualTo("Custom published theme"); + assertThat(publishedTheme.getWorkspaceId()).isNullOrEmpty(); + assertThat(publishedTheme.getApplicationId()).isNullOrEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifact_withoutActionCollection_succeedsWithoutError() { + + FilePart filePart = + createFilePart("test_assets/ImportExportServiceTest/valid-application-without-action-collection.json"); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + + Mono workspaceMono = workspaceService.create(newWorkspace).cache(); + + final Mono resultMono = workspaceMono + .flatMap(workspace -> importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspace.getId(), null, ArtifactJsonType.APPLICATION)) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO); + + List permissionGroups = workspaceMono + .flatMapMany(savedWorkspace -> { + Set defaultPermissionGroups = savedWorkspace.getDefaultPermissionGroups(); + return permissionGroupRepository.findAllById(defaultPermissionGroups); + }) + .collectList() + .block(); + + PermissionGroup adminPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.ADMINISTRATOR)) + .findFirst() + .get(); + + PermissionGroup developerPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.DEVELOPER)) + .findFirst() + .get(); + + PermissionGroup viewerPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.VIEWER)) + .findFirst() + .get(); + + Policy manageAppPolicy = Policy.builder() + .permission(MANAGE_APPLICATIONS.getValue()) + .permissionGroups(Set.of(adminPermissionGroup.getId(), developerPermissionGroup.getId())) + .build(); + Policy readAppPolicy = Policy.builder() + .permission(READ_APPLICATIONS.getValue()) + .permissionGroups(Set.of( + adminPermissionGroup.getId(), developerPermissionGroup.getId(), viewerPermissionGroup.getId())) + .build(); + + StepVerifier.create(resultMono.flatMap(applicationImportDTO -> Mono.zip( + Mono.just(applicationImportDTO), + datasourceService + .getAllByWorkspaceIdWithStorages( + applicationImportDTO.getApplication().getWorkspaceId(), + Optional.of(MANAGE_DATASOURCES)) + .collectList(), + getActionsInApplication(applicationImportDTO.getApplication()) + .collectList(), + newPageService + .findByApplicationId( + applicationImportDTO.getApplication().getId(), MANAGE_PAGES, false) + .collectList(), + actionCollectionService + .findAllByApplicationIdAndViewMode( + applicationImportDTO.getApplication().getId(), false, MANAGE_ACTIONS, null) + .collectList(), + workspaceMono.flatMap(workspace -> workspaceService.getDefaultEnvironmentId( + workspace.getId(), environmentPermission.getExecutePermission()))))) + .assertNext(tuple -> { + final Application application = tuple.getT1().getApplication(); + final List datasourceList = tuple.getT2(); + final List actionDTOS = tuple.getT3(); + final List pageList = tuple.getT4(); + final List actionCollectionList = tuple.getT5(); + String environmentId = tuple.getT6(); + + assertThat(application.getName()).isEqualTo("valid_application"); + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getPages()).hasSize(2); + assertThat(application.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy)); + assertThat(application.getPublishedPages()).hasSize(1); + assertThat(application.getModifiedBy()).isEqualTo("api_user"); + assertThat(application.getUpdatedAt()).isNotNull(); + + assertThat(datasourceList).isNotEmpty(); + datasourceList.forEach(datasource -> { + assertThat(datasource.getWorkspaceId()).isEqualTo(application.getWorkspaceId()); + DatasourceStorageDTO storageDTO = + datasource.getDatasourceStorages().get(environmentId); + assertThat(storageDTO.getDatasourceConfiguration()).isNotNull(); + }); + + assertThat(actionDTOS).isNotEmpty(); + actionDTOS.forEach(actionDTO -> { + assertThat(actionDTO.getPageId()) + .isNotEqualTo(pageList.get(0).getName()); + }); + + assertThat(actionCollectionList).isEmpty(); + + assertThat(pageList).hasSize(2); + + ApplicationPage defaultAppPage = application.getPages().stream() + .filter(ApplicationPage::getIsDefault) + .findFirst() + .orElse(null); + assertThat(defaultAppPage).isNotNull(); + + PageDTO defaultPageDTO = pageList.stream() + .filter(pageDTO -> pageDTO.getId().equals(defaultAppPage.getId())) + .findFirst() + .orElse(null); + + assertThat(defaultPageDTO).isNotNull(); + assertThat(defaultPageDTO.getLayouts().get(0).getLayoutOnLoadActions()) + .isNotEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifact_WithoutThemes_LegacyThemesAssigned() { + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application-without-theme.json"); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + + final Mono resultMono = workspaceService + .create(newWorkspace) + .flatMap(workspace -> importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspace.getId(), null, ArtifactJsonType.APPLICATION)) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO); + + StepVerifier.create(resultMono) + .assertNext(applicationImportDTO -> { + assertThat(applicationImportDTO.getApplication().getEditModeThemeId()) + .isNotEmpty(); + assertThat(applicationImportDTO.getApplication().getPublishedModeThemeId()) + .isNotEmpty(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifact_withoutPageIdInActionCollection_succeeds() { + + FilePart filePart = createFilePart( + "test_assets/ImportExportServiceTest/invalid-application-without-pageId-action-collection.json"); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + + final Mono resultMono = workspaceService + .create(newWorkspace) + .flatMap(workspace -> importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspace.getId(), null, ArtifactJsonType.APPLICATION)) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO); + + StepVerifier.create(resultMono.flatMap(applicationImportDTO -> Mono.zip( + Mono.just(applicationImportDTO), + datasourceService + .getAllByWorkspaceIdWithStorages( + applicationImportDTO.getApplication().getWorkspaceId(), + Optional.of(MANAGE_DATASOURCES)) + .collectList(), + getActionsInApplication(applicationImportDTO.getApplication()) + .collectList(), + newPageService + .findByApplicationId( + applicationImportDTO.getApplication().getId(), MANAGE_PAGES, false) + .collectList(), + actionCollectionService + .findAllByApplicationIdAndViewMode( + applicationImportDTO.getApplication().getId(), false, MANAGE_ACTIONS, null) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1().getApplication(); + final List datasourceList = tuple.getT2(); + final List actionDTOS = tuple.getT3(); + final List pageList = tuple.getT4(); + final List actionCollectionList = tuple.getT5(); + + assertThat(datasourceList).isNotEmpty(); + + assertThat(actionDTOS).hasSize(1); + actionDTOS.forEach(actionDTO -> { + assertThat(actionDTO.getPageId()) + .isNotEqualTo(pageList.get(0).getName()); + }); + + assertThat(actionCollectionList).isEmpty(); + }) + .verifyComplete(); + } + + // this test would be re-written post export flow is completed + @Test + @WithUserDetails(value = "api_user") + public void exportImportApplication_importWithBranchName_updateApplicationResourcesWithBranch() { + Application testApplication = new Application(); + testApplication.setName("Export-Import-Update-Branch_Test-App"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("testBranch"); + testApplication.setGitApplicationMetadata(gitData); + + Application savedApplication = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + Mono result = newPageService + .findNewPagesByApplicationId(savedApplication.getId(), READ_PAGES) + .collectList() + .flatMap(newPages -> { + NewPage newPage = newPages.get(0); + + ActionDTO action = new ActionDTO(); + action.setName("validAction"); + action.setPageId(newPage.getId()); + action.setExecuteOnLoad(true); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(datasourceMap.get("DS1")); + return layoutActionService + .createAction(action) + .flatMap(createdAction -> newActionService.findById(createdAction.getId(), READ_ACTIONS)); + }) + .then(exportApplicationService + .exportApplicationById(savedApplication.getId(), SerialiseApplicationObjective.VERSION_CONTROL) + .flatMap(applicationJson -> importService.importArtifactInWorkspaceFromGit( + workspaceId, savedApplication.getId(), applicationJson, gitData.getBranchName()))) + .map(importableArtifact -> (Application) importableArtifact) + .cache(); + + Mono> updatedPagesMono = result.then(newPageService + .findNewPagesByApplicationId(savedApplication.getId(), READ_PAGES) + .collectList()); + + Mono> updatedActionsMono = result.then(newActionService + .findAllByApplicationIdAndViewMode(savedApplication.getId(), false, READ_PAGES, null) + .collectList()); + + StepVerifier.create(Mono.zip(result, updatedPagesMono, updatedActionsMono)) + .assertNext(tuple -> { + Application application = tuple.getT1(); + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + + final String branchName = + application.getGitApplicationMetadata().getBranchName(); + pageList.forEach(page -> { + assertThat(page.getDefaultResources()).isNotNull(); + assertThat(page.getDefaultResources().getBranchName()).isEqualTo(branchName); + }); + + actionList.forEach(action -> { + assertThat(action.getDefaultResources()).isNotNull(); + assertThat(action.getDefaultResources().getBranchName()).isEqualTo(branchName); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_incompatibleJsonFile_throwException() { + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/incompatible_version.json"); + Mono resultMono = importService + .extractArtifactExchangeJsonAndSaveArtifact(filePart, workspaceId, null, ArtifactJsonType.APPLICATION) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO); + + StepVerifier.create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException + && throwable.getMessage().equals(AppsmithError.INCOMPATIBLE_IMPORTED_JSON.getMessage())) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_withUnConfiguredDatasources_Success() { + FilePart filePart = createFilePart( + "test_assets/ImportExportServiceTest/valid-application-with-un-configured-datasource.json"); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + + Mono workspaceMono = workspaceService.create(newWorkspace).cache(); + + final Mono resultMono = workspaceMono + .flatMap(workspace -> importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspace.getId(), null, ArtifactJsonType.APPLICATION)) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO); + + List permissionGroups = workspaceMono + .flatMapMany(savedWorkspace -> { + Set defaultPermissionGroups = savedWorkspace.getDefaultPermissionGroups(); + return permissionGroupRepository.findAllById(defaultPermissionGroups); + }) + .collectList() + .block(); + + PermissionGroup adminPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.ADMINISTRATOR)) + .findFirst() + .get(); + + PermissionGroup developerPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.DEVELOPER)) + .findFirst() + .get(); + + PermissionGroup viewerPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.VIEWER)) + .findFirst() + .get(); + + Policy manageAppPolicy = Policy.builder() + .permission(MANAGE_APPLICATIONS.getValue()) + .permissionGroups(Set.of(adminPermissionGroup.getId(), developerPermissionGroup.getId())) + .build(); + Policy readAppPolicy = Policy.builder() + .permission(READ_APPLICATIONS.getValue()) + .permissionGroups(Set.of( + adminPermissionGroup.getId(), developerPermissionGroup.getId(), viewerPermissionGroup.getId())) + .build(); + + StepVerifier.create(resultMono.flatMap(applicationImportDTO -> { + Application application = applicationImportDTO.getApplication(); + return Mono.zip( + Mono.just(applicationImportDTO), + datasourceService + .getAllByWorkspaceIdWithStorages( + application.getWorkspaceId(), Optional.of(MANAGE_DATASOURCES)) + .collectList(), + newActionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList(), + newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList(), + actionCollectionService + .findAllByApplicationIdAndViewMode(application.getId(), false, MANAGE_ACTIONS, null) + .collectList()); + })) + .assertNext(tuple -> { + final Application application = tuple.getT1().getApplication(); + final List unConfiguredDatasourceList = + tuple.getT1().getUnConfiguredDatasourceList(); + final boolean isPartialImport = tuple.getT1().getIsPartialImport(); + final List datasourceList = tuple.getT2(); + final List actionList = tuple.getT3(); + final List pageList = tuple.getT4(); + final List actionCollectionList = tuple.getT5(); + + assertThat(application.getName()).isEqualTo("importExportTest"); + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getPages()).hasSize(1); + assertThat(application.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy)); + assertThat(application.getPublishedPages()).hasSize(1); + assertThat(application.getModifiedBy()).isEqualTo("api_user"); + assertThat(application.getUpdatedAt()).isNotNull(); + assertThat(application.getEditModeThemeId()).isNotNull(); + assertThat(application.getPublishedModeThemeId()).isNotNull(); + assertThat(isPartialImport).isEqualTo(Boolean.TRUE); + assertThat(unConfiguredDatasourceList.size()).isNotEqualTo(0); + + assertThat(datasourceList).isNotEmpty(); + List datasourceNames = unConfiguredDatasourceList.stream() + .map(Datasource::getName) + .collect(Collectors.toList()); + assertThat(datasourceNames).contains("mongoDatasource", "postgresTest"); + + List collectionIdInAction = new ArrayList<>(); + assertThat(actionList).isNotEmpty(); + actionList.forEach(newAction -> { + ActionDTO actionDTO = newAction.getUnpublishedAction(); + assertThat(actionDTO.getPageId()) + .isNotEqualTo(pageList.get(0).getName()); + if (!StringUtils.isEmpty(actionDTO.getCollectionId())) { + collectionIdInAction.add(actionDTO.getCollectionId()); + } + }); + + assertThat(actionCollectionList).isEmpty(); + + assertThat(pageList).hasSize(1); + + ApplicationPage defaultAppPage = application.getPages().stream() + .filter(ApplicationPage::getIsDefault) + .findFirst() + .orElse(null); + assertThat(defaultAppPage).isNotNull(); + + PageDTO defaultPageDTO = pageList.stream() + .filter(pageDTO -> pageDTO.getId().equals(defaultAppPage.getId())) + .findFirst() + .orElse(null); + + assertThat(defaultPageDTO).isNotNull(); + }) + .verifyComplete(); + } + + public void importArtifactIntoWorkspace_pageRemovedAndUpdatedDefaultPageNameInBranchApplication_Success() { + Application testApplication = new Application(); + testApplication.setName("importApplicationIntoWorkspace_pageRemovedInBranchApplication_Success"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("master"); + testApplication.setGitApplicationMetadata(gitData); + + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + String gitSyncIdBeforeImport = newPageService + .findById(application.getPages().get(0).getId(), MANAGE_PAGES) + .block() + .getGitSyncId(); + + PageDTO page = new PageDTO(); + page.setName("Page 2"); + page.setApplicationId(application.getId()); + PageDTO savedPage = applicationPageService.createPage(page).block(); + + assert application.getId() != null; + Set applicationPageIdsBeforeImport = + Objects.requireNonNull(applicationRepository + .findById(application.getId()) + .block()) + .getPages() + .stream() + .map(ApplicationPage::getId) + .collect(Collectors.toSet()); + + ApplicationJson applicationJson = createAppJson( + "test_assets/ImportExportServiceTest/valid-application-with-page-removed.json") + .block(); + applicationJson.getPageList().get(0).setGitSyncId(gitSyncIdBeforeImport); + + Application importedApplication = importService + .importArtifactInWorkspaceFromGit(workspaceId, application.getId(), applicationJson, "master") + .map(artifact -> (Application) artifact) + .block(); + + assert importedApplication != null; + Mono> pageList = Flux.fromIterable(importedApplication.getPages().stream() + .map(ApplicationPage::getId) + .collect(Collectors.toList())) + .flatMap(s -> newPageService.findById(s, MANAGE_PAGES)) + .collectList(); + + StepVerifier.create(pageList) + .assertNext(newPages -> { + // Check before import we had both the pages + assertThat(applicationPageIdsBeforeImport).hasSize(2); + assertThat(applicationPageIdsBeforeImport).contains(savedPage.getId()); + + assertThat(newPages.size()).isEqualTo(1); + assertThat(importedApplication.getPages().size()).isEqualTo(1); + assertThat(importedApplication.getPages().get(0).getId()) + .isEqualTo(newPages.get(0).getId()); + assertThat(newPages.get(0).getPublishedPage().getName()).isEqualTo("importedPage"); + assertThat(newPages.get(0).getGitSyncId()).isEqualTo(gitSyncIdBeforeImport); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importArtifactIntoWorkspace_pageAddedInBranchApplication_Success() { + Application testApplication = new Application(); + testApplication.setName("importApplicationIntoWorkspace_pageAddedInBranchApplication_Success"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("master"); + testApplication.setGitApplicationMetadata(gitData); + + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + String gitSyncIdBeforeImport = newPageService + .findById(application.getPages().get(0).getId(), MANAGE_PAGES) + .block() + .getGitSyncId(); + + assert application.getId() != null; + Set applicationPageIdsBeforeImport = + Objects.requireNonNull(applicationRepository + .findById(application.getId()) + .block()) + .getPages() + .stream() + .map(ApplicationPage::getId) + .collect(Collectors.toSet()); + + ApplicationJson applicationJson = createAppJson( + "test_assets/ImportExportServiceTest/valid-application-with-page-added.json") + .block(); + applicationJson.getPageList().get(0).setGitSyncId(gitSyncIdBeforeImport); + + Application applicationMono = importService + .importArtifactInWorkspaceFromGit(workspaceId, application.getId(), applicationJson, "master") + .map(artifact -> (Application) artifact) + .block(); + + Mono> pageList = Flux.fromIterable(applicationMono.getPages().stream() + .map(ApplicationPage::getId) + .collect(Collectors.toList())) + .flatMap(s -> newPageService.findById(s, MANAGE_PAGES)) + .collectList(); + + StepVerifier.create(pageList) + .assertNext(newPages -> { + // Check before import we had both the pages + assertThat(applicationPageIdsBeforeImport).hasSize(1); + assertThat(newPages.size()).isEqualTo(3); + List pageNames = newPages.stream() + .map(newPage -> newPage.getUnpublishedPage().getName()) + .collect(Collectors.toList()); + assertThat(pageNames).contains("Page1"); + assertThat(pageNames).contains("Page2"); + assertThat(pageNames).contains("Page3"); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importUpdatedApplicationIntoWorkspaceFromFile_publicApplication_visibilityFlagNotReset() { + // Create a application and make it public + // Now add a page and export the same import it to the app + // Check if the policies and visibility flag are not reset + + Mono workspaceResponse = workspaceService.findById(workspaceId, READ_WORKSPACES); + + List permissionGroups = workspaceResponse + .flatMapMany(savedWorkspace -> { + Set defaultPermissionGroups = savedWorkspace.getDefaultPermissionGroups(); + return permissionGroupRepository.findAllById(defaultPermissionGroups); + }) + .collectList() + .block(); + + PermissionGroup adminPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.ADMINISTRATOR)) + .findFirst() + .get(); + + PermissionGroup developerPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.DEVELOPER)) + .findFirst() + .get(); + + PermissionGroup viewerPermissionGroup = permissionGroups.stream() + .filter(permissionGroup -> permissionGroup.getName().startsWith(FieldName.VIEWER)) + .findFirst() + .get(); + + Application testApplication = new Application(); + testApplication.setName( + "importUpdatedApplicationIntoWorkspaceFromFile_publicApplication_visibilityFlagNotReset"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("master"); + testApplication.setGitApplicationMetadata(gitData); + + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + ApplicationAccessDTO applicationAccessDTO = new ApplicationAccessDTO(); + applicationAccessDTO.setPublicAccess(true); + Application newApplication = applicationService + .changeViewAccess(application.getId(), "master", applicationAccessDTO) + .block(); + + PermissionGroup anonymousPermissionGroup = + permissionGroupService.getPublicPermissionGroup().block(); + + Policy manageAppPolicy = Policy.builder() + .permission(MANAGE_APPLICATIONS.getValue()) + .permissionGroups(Set.of(adminPermissionGroup.getId(), developerPermissionGroup.getId())) + .build(); + Policy readAppPolicy = Policy.builder() + .permission(READ_APPLICATIONS.getValue()) + .permissionGroups(Set.of( + adminPermissionGroup.getId(), + developerPermissionGroup.getId(), + viewerPermissionGroup.getId(), + anonymousPermissionGroup.getId())) + .build(); + + Mono applicationMono = exportApplicationService + .exportApplicationById(application.getId(), "master") + .flatMap(applicationJson -> importService + .importArtifactInWorkspaceFromGit(workspaceId, application.getId(), applicationJson, "master") + .map(artifact -> (Application) artifact)); + + StepVerifier.create(applicationMono) + .assertNext(application1 -> { + assertThat(application1.getIsPublic()).isEqualTo(Boolean.TRUE); + assertThat(application1.getPolicies()).containsAll(Set.of(manageAppPolicy, readAppPolicy)); + }) + .verifyComplete(); + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org + * 2. Add new page to the imported application + * 3. User tries to import application from same application json file + * 4. Added page will be removed + */ + @Test + @WithUserDetails(value = "api_user") + public void discardChange_addNewPageAfterImport_addedPageRemoved() { + + /* + 1. Import application + 2. Add single page to imported app + 3. Import the application from same JSON with applicationId + 4. Added page should be deleted from DB + */ + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + String workspaceId = createTemplateWorkspace().getId(); + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson.getExportedApplication().setName("discard-change-page-added"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + PageDTO page = new PageDTO(); + page.setName("discard-page-test"); + page.setApplicationId(application.getId()); + return applicationPageService.createPage(page); + }) + .flatMap(page -> applicationRepository.findById(page.getApplicationId())) + .cache(); + List pageListBefore = resultMonoWithoutDiscardOperation + .flatMap(application -> newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList()) + .block(); + + StepVerifier.create(resultMonoWithoutDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List pageList = tuple.getT2(); + + assertThat(application.getName()).isEqualTo("discard-change-page-added"); + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getPages()).hasSize(3); + assertThat(application.getPublishedPages()).hasSize(1); + assertThat(application.getModifiedBy()).isEqualTo("api_user"); + assertThat(application.getUpdatedAt()).isNotNull(); + assertThat(application.getEditModeThemeId()).isNotNull(); + assertThat(application.getPublishedModeThemeId()).isNotNull(); + + assertThat(pageList).hasSize(3); + + ApplicationPage defaultAppPage = application.getPages().stream() + .filter(ApplicationPage::getIsDefault) + .findFirst() + .orElse(null); + assertThat(defaultAppPage).isNotNull(); + + PageDTO defaultPageDTO = pageList.stream() + .filter(pageDTO -> pageDTO.getId().equals(defaultAppPage.getId())) + .findFirst() + .orElse(null); + + assertThat(defaultPageDTO).isNotNull(); + assertThat(defaultPageDTO.getLayouts().get(0).getLayoutOnLoadActions()) + .isNotEmpty(); + + List pageNames = new ArrayList<>(); + pageList.forEach(page -> pageNames.add(page.getName())); + assertThat(pageNames).contains("discard-page-test"); + }) + .verifyComplete(); + + // Import the same application again to find if the added page is deleted + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List pageList = tuple.getT2(); + + assertThat(application.getPages()).hasSize(2); + assertThat(application.getPublishedPages()).hasSize(1); + + assertThat(pageList).hasSize(2); + for (PageDTO page : pageList) { + PageDTO curentPage = pageListBefore.stream() + .filter(pageDTO -> pageDTO.getId().equals(page.getId())) + .collect(Collectors.toList()) + .get(0); + assertThat(page.getPolicies()).isEqualTo(curentPage.getPolicies()); + } + + List pageNames = new ArrayList<>(); + pageList.forEach(page -> pageNames.add(page.getName())); + assertThat(pageNames).doesNotContain("discard-page-test"); + }) + .verifyComplete(); + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org + * 2. Add new action to the imported application + * 3. User tries to import application from same application json file + * 4. Added action will be removed + */ + @Test + @WithUserDetails(value = "api_user") + public void discardChange_addNewActionAfterImport_addedActionRemoved() { + + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + String workspaceId = createTemplateWorkspace().getId(); + + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson.getExportedApplication().setName("discard-change-action-added"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(datasourceMap.get("DS1")); + action.setName("discard-action-test"); + action.setPageId(application.getPages().get(0).getId()); + return layoutActionService.createAction(action); + }) + .flatMap(actionDTO -> newActionService.getById(actionDTO.getId())) + .flatMap(newAction -> applicationRepository.findById(newAction.getApplicationId())) + .cache(); + + List actionListBefore = resultMonoWithoutDiscardOperation + .flatMap(application -> getActionsInApplication(application).collectList()) + .block(); + + StepVerifier.create(resultMonoWithoutDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + getActionsInApplication(application).collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List actionList = tuple.getT2(); + + assertThat(application.getName()).isEqualTo("discard-change-action-added"); + assertThat(application.getWorkspaceId()).isNotNull(); + + List actionNames = new ArrayList<>(); + actionList.forEach(actionDTO -> actionNames.add(actionDTO.getName())); + assertThat(actionNames).contains("discard-action-test"); + }) + .verifyComplete(); + + // Import the same application again + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + getActionsInApplication(application).collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List actionList = tuple.getT2(); + + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getServerSchemaVersion()).isNotNull(); + assertThat(application.getClientSchemaVersion()).isNotNull(); + + List actionNames = new ArrayList<>(); + actionList.forEach(actionDTO -> actionNames.add(actionDTO.getName())); + assertThat(actionNames).doesNotContain("discard-action-test"); + for (ActionDTO action : actionListBefore) { + ActionDTO currentAction = actionListBefore.stream() + .filter(actionDTO -> actionDTO.getId().equals(action.getId())) + .collect(Collectors.toList()) + .get(0); + assertThat(action.getPolicies()).isEqualTo(currentAction.getPolicies()); + } + }) + .verifyComplete(); + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org + * 2. Add actionCollection to the imported application + * 3. User tries to import application from same application json file + * 4. Added actionCollection will be removed + */ + @Test + @WithUserDetails(value = "api_user") + public void discardChange_addNewActionCollectionAfterImport_addedActionCollectionRemoved() { + + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application-without-action-collection.json"); + String workspaceId = createTemplateWorkspace().getId(); + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson.getExportedApplication().setName("discard-change-collection-added"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + ActionCollectionDTO actionCollectionDTO1 = new ActionCollectionDTO(); + actionCollectionDTO1.setName("discard-action-collection-test"); + actionCollectionDTO1.setPageId(application.getPages().get(0).getId()); + actionCollectionDTO1.setApplicationId(application.getId()); + actionCollectionDTO1.setWorkspaceId(application.getWorkspaceId()); + actionCollectionDTO1.setPluginId(jsDatasource.getPluginId()); + ActionDTO action1 = new ActionDTO(); + action1.setName("discard-action-collection-test-action"); + action1.setActionConfiguration(new ActionConfiguration()); + action1.getActionConfiguration().setBody("mockBody"); + actionCollectionDTO1.setActions(List.of(action1)); + actionCollectionDTO1.setPluginType(PluginType.JS); + + return layoutCollectionService.createCollection(actionCollectionDTO1, null); + }) + .flatMap(actionCollectionDTO -> actionCollectionService.getById(actionCollectionDTO.getId())) + .flatMap(actionCollection -> applicationRepository.findById(actionCollection.getApplicationId())) + .cache(); + + List actionCollectionListBefore = resultMonoWithoutDiscardOperation + .flatMap(application -> actionCollectionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList()) + .block(); + List actionListBefore = resultMonoWithoutDiscardOperation + .flatMap(application -> getActionsInApplication(application).collectList()) + .block(); + + StepVerifier.create(resultMonoWithoutDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + actionCollectionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList(), + getActionsInApplication(application).collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List actionCollectionList = tuple.getT2(); + final List actionList = tuple.getT3(); + + assertThat(application.getName()).isEqualTo("discard-change-collection-added"); + assertThat(application.getWorkspaceId()).isNotNull(); + + List actionCollectionNames = new ArrayList<>(); + actionCollectionList.forEach(actionCollection -> actionCollectionNames.add( + actionCollection.getUnpublishedCollection().getName())); + assertThat(actionCollectionNames).contains("discard-action-collection-test"); + + List actionNames = new ArrayList<>(); + actionList.forEach(actionDTO -> actionNames.add(actionDTO.getName())); + assertThat(actionNames).contains("discard-action-collection-test-action"); + }) + .verifyComplete(); + + // Import the same application again + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + actionCollectionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList(), + getActionsInApplication(application).collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List actionCollectionList = tuple.getT2(); + final List actionList = tuple.getT3(); + + assertThat(application.getWorkspaceId()).isNotNull(); + + List actionCollectionNames = new ArrayList<>(); + actionCollectionList.forEach(actionCollection -> actionCollectionNames.add( + actionCollection.getUnpublishedCollection().getName())); + assertThat(actionCollectionNames).doesNotContain("discard-action-collection-test"); + + List actionNames = new ArrayList<>(); + actionList.forEach(actionDTO -> actionNames.add(actionDTO.getName())); + assertThat(actionNames).doesNotContain("discard-action-collection-test-action"); + for (ActionDTO action : actionListBefore) { + ActionDTO currentAction = actionListBefore.stream() + .filter(actionDTO -> actionDTO.getId().equals(action.getId())) + .collect(Collectors.toList()) + .get(0); + assertThat(action.getPolicies()).isEqualTo(currentAction.getPolicies()); + } + + for (ActionCollection actionCollection : actionCollectionListBefore) { + ActionCollection currentAction = actionCollectionListBefore.stream() + .filter(actionDTO -> actionDTO.getId().equals(actionCollection.getId())) + .collect(Collectors.toList()) + .get(0); + assertThat(actionCollection.getPolicies()).isEqualTo(currentAction.getPolicies()); + } + }) + .verifyComplete(); + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org + * 2. Remove existing page from imported application + * 3. Import application from same application json file + * 4. Removed page will be restored + */ + @Test + @WithUserDetails(value = "api_user") + public void discardChange_removeNewPageAfterImport_removedPageRestored() { + + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + String workspaceId = createTemplateWorkspace().getId(); + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson.getExportedApplication().setName("discard-change-page-removed"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + Optional applicationPage = application.getPages().stream() + .filter(page -> !page.isDefault()) + .findFirst(); + return applicationPageService.deleteUnpublishedPage( + applicationPage.get().getId()); + }) + .flatMap(page -> applicationRepository.findById(page.getApplicationId())) + .cache(); + + StepVerifier.create(resultMonoWithoutDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List pageList = tuple.getT2(); + + assertThat(application.getName()).isEqualTo("discard-change-page-removed"); + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getPages()).hasSize(1); + + assertThat(pageList).hasSize(1); + }) + .verifyComplete(); + + // Import the same application again + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List pageList = tuple.getT2(); + + assertThat(application.getPages()).hasSize(2); + assertThat(application.getPublishedPages()).hasSize(1); + + assertThat(pageList).hasSize(2); + }) + .verifyComplete(); + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org + * 2. Remove existing action from imported application + * 3. Import application from same application json file + * 4. Removed action will be restored + */ + @Test + @WithUserDetails(value = "api_user") + public void discardChange_removeNewActionAfterImport_removedActionRestored() { + + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + String workspaceId = createTemplateWorkspace().getId(); + final String[] deletedActionName = new String[1]; + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson.getExportedApplication().setName("discard-change-action-removed"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + return getActionsInApplication(application) + .next() + .flatMap(actionDTO -> { + deletedActionName[0] = actionDTO.getName(); + return newActionService.deleteUnpublishedAction(actionDTO.getId()); + }) + .then(applicationPageService.publish(application.getId(), true)); + }) + .cache(); + + StepVerifier.create(resultMonoWithoutDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + getActionsInApplication(application).collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List actionList = tuple.getT2(); + + assertThat(application.getName()).isEqualTo("discard-change-action-removed"); + assertThat(application.getWorkspaceId()).isNotNull(); + + List actionNames = new ArrayList<>(); + actionList.forEach(actionDTO -> actionNames.add(actionDTO.getName())); + assertThat(actionNames).doesNotContain(deletedActionName[0]); + }) + .verifyComplete(); + + // Import the same application again + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + getActionsInApplication(application).collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List actionList = tuple.getT2(); + + assertThat(application.getWorkspaceId()).isNotNull(); + + List actionNames = new ArrayList<>(); + actionList.forEach(actionDTO -> actionNames.add(actionDTO.getName())); + assertThat(actionNames).contains(deletedActionName[0]); + }) + .verifyComplete(); + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org + * 2. Remove existing actionCollection from imported application + * 3. Import application from same application json file + * 4. Removed actionCollection along-with actions will be restored + */ + @Test + @WithUserDetails(value = "api_user") + public void discardChange_removeNewActionCollection_removedActionCollectionRestored() { + + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + String workspaceId = createTemplateWorkspace().getId(); + final String[] deletedActionCollectionNames = new String[1]; + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson.getExportedApplication().setName("discard-change-collection-removed"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + return actionCollectionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .next() + .flatMap(actionCollection -> { + deletedActionCollectionNames[0] = actionCollection + .getUnpublishedCollection() + .getName(); + return actionCollectionService.deleteUnpublishedActionCollection( + actionCollection.getId()); + }) + .then(applicationPageService.publish(application.getId(), true)); + }) + .cache(); + + StepVerifier.create(resultMonoWithoutDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + actionCollectionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List actionCollectionList = tuple.getT2(); + + assertThat(application.getName()).isEqualTo("discard-change-collection-removed"); + assertThat(application.getWorkspaceId()).isNotNull(); + + List actionCollectionNames = new ArrayList<>(); + actionCollectionList.forEach(actionCollection -> actionCollectionNames.add( + actionCollection.getUnpublishedCollection().getName())); + assertThat(actionCollectionNames).doesNotContain(deletedActionCollectionNames); + }) + .verifyComplete(); + + // Import the same application again + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + actionCollectionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List actionCollectionList = tuple.getT2(); + + assertThat(application.getWorkspaceId()).isNotNull(); + + List actionCollectionNames = new ArrayList<>(); + actionCollectionList.forEach(actionCollection -> actionCollectionNames.add( + actionCollection.getUnpublishedCollection().getName())); + assertThat(actionCollectionNames).contains(deletedActionCollectionNames); + }) + .verifyComplete(); + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org + * 2. Add Navigation Settings to the imported application + * 3. User tries to import application from same application json file + * 4. Added NavigationSetting will be removed + */ + @Test + @WithUserDetails(value = "api_user") + public void discardChange_addNavigationAndThemeSettingAfterImport_addedNavigationAndThemeSettingRemoved() { + Mono applicationJsonMono = createAppJson( + "test_assets/ImportExportServiceTest/valid-application-without-navigation-theme-setting.json"); + String workspaceId = createTemplateWorkspace().getId(); + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson.getExportedApplication().setName("discard-change-navsettings-added"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + ApplicationDetail applicationDetail = new ApplicationDetail(); + Application.NavigationSetting navigationSetting = new Application.NavigationSetting(); + navigationSetting.setOrientation("top"); + applicationDetail.setNavigationSetting(navigationSetting); + + Application.ThemeSetting themeSettings = getThemeSetting(); + applicationDetail.setThemeSetting(themeSettings); + + application.setUnpublishedApplicationDetail(applicationDetail); + application.setPublishedApplicationDetail(applicationDetail); + return applicationService.save(application); + }) + .cache(); + + StepVerifier.create(resultMonoWithoutDiscardOperation) + .assertNext(initialApplication -> { + assertThat(initialApplication.getUnpublishedApplicationDetail()) + .isNotNull(); + assertThat(initialApplication + .getUnpublishedApplicationDetail() + .getNavigationSetting()) + .isNotNull(); + assertThat(initialApplication + .getUnpublishedApplicationDetail() + .getNavigationSetting() + .getOrientation()) + .isEqualTo("top"); + assertThat(initialApplication.getPublishedApplicationDetail()) + .isNotNull(); + assertThat(initialApplication + .getPublishedApplicationDetail() + .getNavigationSetting()) + .isNotNull(); + assertThat(initialApplication + .getPublishedApplicationDetail() + .getNavigationSetting() + .getOrientation()) + .isEqualTo("top"); + + Application.ThemeSetting themes = + initialApplication.getApplicationDetail().getThemeSetting(); + assertThat(themes.getAccentColor()).isEqualTo("#FFFFFF"); + assertThat(themes.getBorderRadius()).isEqualTo("#000000"); + assertThat(themes.getColorMode()).isEqualTo(Application.ThemeSetting.Type.LIGHT); + assertThat(themes.getDensity()).isEqualTo(1); + assertThat(themes.getFontFamily()).isEqualTo("#000000"); + assertThat(themes.getSizing()).isEqualTo(1); + }) + .verifyComplete(); + // Import the same application again + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation) + .assertNext(application -> { + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getUnpublishedApplicationDetail()).isNull(); + assertThat(application.getPublishedApplicationDetail()).isNull(); + }) + .verifyComplete(); + } + + @NotNull private static Application.ThemeSetting getThemeSetting() { + Application.ThemeSetting themeSettings = new Application.ThemeSetting(); + themeSettings.setSizing(1); + themeSettings.setDensity(1); + themeSettings.setBorderRadius("#000000"); + themeSettings.setAccentColor("#FFFFFF"); + themeSettings.setFontFamily("#000000"); + themeSettings.setColorMode(Application.ThemeSetting.Type.LIGHT); + return themeSettings; + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org + * 2. Updated App Layout Settings to the imported application + * 3. User tries to import application from same application json file + * 4. Added App Layout will be removed + */ + @Test + @WithUserDetails(value = "api_user") + public void discardChange_addAppLayoutAfterImport_addedAppLayoutRemoved() { + + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application-without-app-layout.json"); + String workspaceId = createTemplateWorkspace().getId(); + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson.getExportedApplication().setName("discard-change-applayout-added"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + application.setUnpublishedAppLayout(new Application.AppLayout(Application.AppLayout.Type.DESKTOP)); + application.setPublishedAppLayout(new Application.AppLayout(Application.AppLayout.Type.DESKTOP)); + return applicationService.save(application); + }) + .cache(); + + StepVerifier.create(resultMonoWithoutDiscardOperation) + .assertNext(initialApplication -> { + assertThat(initialApplication.getUnpublishedAppLayout()).isNotNull(); + assertThat(initialApplication.getUnpublishedAppLayout().getType()) + .isEqualTo(Application.AppLayout.Type.DESKTOP); + assertThat(initialApplication.getPublishedAppLayout()).isNotNull(); + assertThat(initialApplication.getPublishedAppLayout().getType()) + .isEqualTo(Application.AppLayout.Type.DESKTOP); + }) + .verifyComplete(); + + // Import the same application again + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation) + .assertNext(application -> { + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getUnpublishedAppLayout()).isNull(); + assertThat(application.getPublishedAppLayout()).isNull(); + }) + .verifyComplete(); + } + + /** + * Testcase for checking the discard changes flow for following events: + * 1. Import application in org which has app positioning in applicationDetail already added + * 2. Add Navigation Settings to the imported application + * 3. User tries to import application from same application json file + * 4. Added NavigationSetting will be removed + */ + @Test + @WithUserDetails(value = "api_user") + public void + discardChange_addNavigationSettingAfterAppPositioningAlreadyPresentInImport_addedNavigationSettingRemoved() { + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application-with-app-positioning.json"); + String workspaceId = createTemplateWorkspace().getId(); + final Mono resultMonoWithoutDiscardOperation = applicationJsonMono + .flatMap(applicationJson -> { + applicationJson + .getExportedApplication() + .setName("discard-change-navsettings-added-appPositioning-present"); + return importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }) + .flatMap(application -> { + ApplicationDetail applicationDetail = application.getUnpublishedApplicationDetail(); + Application.NavigationSetting navigationSetting = new Application.NavigationSetting(); + navigationSetting.setOrientation("top"); + applicationDetail.setNavigationSetting(navigationSetting); + application.setUnpublishedApplicationDetail(applicationDetail); + application.setPublishedApplicationDetail(applicationDetail); + return applicationService.save(application); + }) + .cache(); + + StepVerifier.create(resultMonoWithoutDiscardOperation) + .assertNext(initialApplication -> { + assertThat(initialApplication.getUnpublishedApplicationDetail()) + .isNotNull(); + assertThat(initialApplication + .getUnpublishedApplicationDetail() + .getNavigationSetting()) + .isNotNull(); + assertThat(initialApplication + .getUnpublishedApplicationDetail() + .getNavigationSetting() + .getOrientation()) + .isEqualTo("top"); + assertThat(initialApplication.getPublishedApplicationDetail()) + .isNotNull(); + assertThat(initialApplication + .getPublishedApplicationDetail() + .getNavigationSetting()) + .isNotNull(); + assertThat(initialApplication + .getPublishedApplicationDetail() + .getNavigationSetting() + .getOrientation()) + .isEqualTo("top"); + assertThat(initialApplication + .getUnpublishedApplicationDetail() + .getAppPositioning()) + .isNotNull(); + assertThat(initialApplication + .getUnpublishedApplicationDetail() + .getAppPositioning() + .getType()) + .isEqualTo(Application.AppPositioning.Type.AUTO); + assertThat(initialApplication + .getPublishedApplicationDetail() + .getAppPositioning()) + .isNotNull(); + assertThat(initialApplication + .getPublishedApplicationDetail() + .getAppPositioning() + .getType()) + .isEqualTo(Application.AppPositioning.Type.AUTO); + }) + .verifyComplete(); + // Import the same application again + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation.flatMap( + importedApplication -> applicationJsonMono.flatMap(applicationJson -> { + importedApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + importedApplication + .getGitApplicationMetadata() + .setDefaultApplicationId(importedApplication.getId()); + return applicationService + .save(importedApplication) + .then(importService.importArtifactInWorkspaceFromGit( + importedApplication.getWorkspaceId(), + importedApplication.getId(), + applicationJson, + "main")) + .map(importableArtifact -> (Application) importableArtifact); + })); + + StepVerifier.create(resultMonoWithDiscardOperation) + .assertNext(application -> { + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getUnpublishedApplicationDetail()).isNotNull(); + assertThat(application.getPublishedApplicationDetail()).isNotNull(); + assertThat(application.getUnpublishedApplicationDetail().getAppPositioning()) + .isNotNull(); + assertThat(application + .getUnpublishedApplicationDetail() + .getAppPositioning() + .getType()) + .isEqualTo(Application.AppPositioning.Type.AUTO); + assertThat(application.getUnpublishedApplicationDetail().getNavigationSetting()) + .isNull(); + assertThat(application.getPublishedApplicationDetail().getNavigationSetting()) + .isNull(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void applySchemaMigration_jsonFileWithFirstVersion_migratedToLatestVersionSuccess() { + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/file-with-v1.json"); + + Mono stringifiedFile = DataBufferUtils.join(filePart.content()).map(dataBuffer -> { + byte[] data = new byte[dataBuffer.readableByteCount()]; + dataBuffer.read(data); + DataBufferUtils.release(dataBuffer); + return new String(data); + }); + Mono v1ApplicationMono = stringifiedFile + .map(data -> { + return gson.fromJson(data, ApplicationJson.class); + }) + .cache(); + + Mono migratedApplicationMono = v1ApplicationMono.map(applicationJson -> { + ApplicationJson applicationJson1 = new ApplicationJson(); + AppsmithBeanUtils.copyNestedNonNullProperties(applicationJson, applicationJson1); + return JsonSchemaMigration.migrateApplicationToLatestSchema(applicationJson1); + }); + + StepVerifier.create(Mono.zip(v1ApplicationMono, migratedApplicationMono)) + .assertNext(tuple -> { + ApplicationJson v1ApplicationJson = tuple.getT1(); + ApplicationJson latestApplicationJson = tuple.getT2(); + + assertThat(v1ApplicationJson.getServerSchemaVersion()).isEqualTo(1); + assertThat(v1ApplicationJson.getClientSchemaVersion()).isEqualTo(1); + + assertThat(latestApplicationJson.getServerSchemaVersion()) + .isEqualTo(JsonSchemaVersions.serverVersion); + assertThat(latestApplicationJson.getClientSchemaVersion()) + .isEqualTo(JsonSchemaVersions.clientVersion); + }) + .verifyComplete(); + } + + /** + * Testcase to check if the application is exported with the datasource configuration object if this setting is + * enabled from application object + * This can be enabled with exportWithConfiguration: true + */ + @Test + @WithUserDetails(value = "api_user") + public void exportApplication_withDatasourceConfig_exportedWithDecryptedFields() { + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("template-org-with-ds"); + + Application testApplication = new Application(); + testApplication.setName("exportApplication_withCredentialsForSampleApps_SuccessWithDecryptFields"); + testApplication.setExportWithConfiguration(true); + testApplication = applicationPageService + .createApplication(testApplication, workspaceId) + .block(); + assert testApplication != null; + exportWithConfigurationAppId = testApplication.getId(); + ApplicationAccessDTO accessDTO = new ApplicationAccessDTO(); + accessDTO.setPublicAccess(true); + applicationService + .changeViewAccess(exportWithConfigurationAppId, accessDTO) + .block(); + final String appName = testApplication.getName(); + final Mono resultMono = Mono.zip( + Mono.just(testApplication), + newPageService.findPageById( + testApplication.getPages().get(0).getId(), READ_PAGES, false)) + .flatMap(tuple -> { + Application testApp = tuple.getT1(); + PageDTO testPage = tuple.getT2(); + + Layout layout = testPage.getLayouts().get(0); + ObjectMapper objectMapper = new ObjectMapper(); + JSONObject dsl = new JSONObject(); + try { + dsl = new JSONObject(objectMapper.readValue( + DEFAULT_PAGE_LAYOUT, new TypeReference>() {})); + } catch (JsonProcessingException e) { + e.printStackTrace(); + fail(); + } + + ArrayList children = (ArrayList) dsl.get("children"); + JSONObject testWidget = new JSONObject(); + testWidget.put("widgetName", "firstWidget"); + JSONArray temp = new JSONArray(); + temp.add(new JSONObject(Map.of("key", "testField"))); + testWidget.put("dynamicBindingPathList", temp); + testWidget.put("testField", "{{ validAction.data }}"); + children.add(testWidget); + + layout.setDsl(dsl); + layout.setPublishedDsl(dsl); + + ActionDTO action = new ActionDTO(); + action.setName("validAction"); + action.setPageId(testPage.getId()); + action.setExecuteOnLoad(true); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(datasourceMap.get("DS2")); + + ActionDTO action2 = new ActionDTO(); + action2.setName("validAction2"); + action2.setPageId(testPage.getId()); + action2.setExecuteOnLoad(true); + action2.setUserSetOnLoad(true); + ActionConfiguration actionConfiguration2 = new ActionConfiguration(); + actionConfiguration2.setHttpMethod(HttpMethod.GET); + action2.setActionConfiguration(actionConfiguration2); + action2.setDatasource(datasourceMap.get("DS2")); + + ActionCollectionDTO actionCollectionDTO1 = new ActionCollectionDTO(); + actionCollectionDTO1.setName("testCollection1"); + actionCollectionDTO1.setPageId(testPage.getId()); + actionCollectionDTO1.setApplicationId(testApp.getId()); + actionCollectionDTO1.setWorkspaceId(testApp.getWorkspaceId()); + actionCollectionDTO1.setPluginId(jsDatasource.getPluginId()); + ActionDTO action1 = new ActionDTO(); + action1.setName("testAction1"); + action1.setActionConfiguration(new ActionConfiguration()); + action1.getActionConfiguration().setBody("mockBody"); + actionCollectionDTO1.setActions(List.of(action1)); + actionCollectionDTO1.setPluginType(PluginType.JS); + + return layoutCollectionService + .createCollection(actionCollectionDTO1, null) + .then(layoutActionService.createSingleAction(action, Boolean.FALSE)) + .then(layoutActionService.createSingleAction(action2, Boolean.FALSE)) + .then(updateLayoutService.updateLayout( + testPage.getId(), testPage.getApplicationId(), layout.getId(), layout)) + .then(exportApplicationService.exportApplicationById(testApp.getId(), "")); + }) + .cache(); + + Mono> actionListMono = resultMono.then(newActionService + .findAllByApplicationIdAndViewMode(testApplication.getId(), false, READ_ACTIONS, null) + .collectList()); + + Mono> collectionListMono = resultMono.then(actionCollectionService + .findAllByApplicationIdAndViewMode(testApplication.getId(), false, READ_ACTIONS, null) + .collectList()); + + Mono> pageListMono = resultMono.then(newPageService + .findNewPagesByApplicationId(testApplication.getId(), READ_PAGES) + .collectList()); + + StepVerifier.create(Mono.zip(resultMono, actionListMono, collectionListMono, pageListMono)) + .assertNext(tuple -> { + ApplicationJson applicationJson = tuple.getT1(); + List DBActions = tuple.getT2(); + List DBCollections = tuple.getT3(); + List DBPages = tuple.getT4(); + + Application exportedApp = applicationJson.getExportedApplication(); + List pageList = applicationJson.getPageList(); + List actionList = applicationJson.getActionList(); + List actionCollectionList = applicationJson.getActionCollectionList(); + List datasourceList = applicationJson.getDatasourceList(); + + List exportedCollectionIds = actionCollectionList.stream() + .map(ActionCollection::getId) + .collect(Collectors.toList()); + List exportedActionIds = + actionList.stream().map(NewAction::getId).collect(Collectors.toList()); + List DBCollectionIds = + DBCollections.stream().map(ActionCollection::getId).collect(Collectors.toList()); + List DBActionIds = + DBActions.stream().map(NewAction::getId).collect(Collectors.toList()); + List DBOnLayoutLoadActionIds = new ArrayList<>(); + List exportedOnLayoutLoadActionIds = new ArrayList<>(); + + DBPages.forEach( + newPage -> newPage.getUnpublishedPage().getLayouts().forEach(layout -> { + if (layout.getLayoutOnLoadActions() != null) { + layout.getLayoutOnLoadActions().forEach(dslActionDTOSet -> { + dslActionDTOSet.forEach( + actionDTO -> DBOnLayoutLoadActionIds.add(actionDTO.getId())); + }); + } + })); + + pageList.forEach( + newPage -> newPage.getUnpublishedPage().getLayouts().forEach(layout -> { + if (layout.getLayoutOnLoadActions() != null) { + layout.getLayoutOnLoadActions().forEach(dslActionDTOSet -> { + dslActionDTOSet.forEach( + actionDTO -> exportedOnLayoutLoadActionIds.add(actionDTO.getId())); + }); + } + })); + + NewPage defaultPage = pageList.get(0); + + assertThat(exportedApp.getName()).isEqualTo(appName); + assertThat(exportedApp.getWorkspaceId()).isNull(); + assertThat(exportedApp.getPages()).hasSize(1); + ApplicationPage page = exportedApp.getPages().get(0); + + assertThat(page.getId()) + .isEqualTo(defaultPage.getUnpublishedPage().getName()); + assertThat(page.getIsDefault()).isTrue(); + assertThat(page.getDefaultPageId()).isNull(); + + assertThat(exportedApp.getPolicies()).isNull(); + + assertThat(pageList).hasSize(1); + assertThat(defaultPage.getApplicationId()).isNull(); + assertThat(defaultPage + .getUnpublishedPage() + .getLayouts() + .get(0) + .getDsl()) + .isNotNull(); + assertThat(defaultPage.getId()).isNull(); + assertThat(defaultPage.getPolicies()).isNull(); + + assertThat(actionList.isEmpty()).isFalse(); + assertThat(actionList).hasSize(3); + NewAction validAction = actionList.stream() + .filter(action -> action.getId().equals("Page1_validAction")) + .findFirst() + .get(); + assertThat(validAction.getApplicationId()).isNull(); + assertThat(validAction.getPluginId()).isEqualTo(installedPlugin.getPackageName()); + assertThat(validAction.getPluginType()).isEqualTo(PluginType.API); + assertThat(validAction.getWorkspaceId()).isNull(); + assertThat(validAction.getPolicies()).isNull(); + assertThat(validAction.getId()).isNotNull(); + ActionDTO unpublishedAction = validAction.getUnpublishedAction(); + assertThat(unpublishedAction.getPageId()) + .isEqualTo(defaultPage.getUnpublishedPage().getName()); + assertThat(unpublishedAction.getDatasource().getPluginId()) + .isEqualTo(installedPlugin.getPackageName()); + + NewAction testAction1 = actionList.stream() + .filter(action -> + action.getUnpublishedAction().getName().equals("testAction1")) + .findFirst() + .get(); + assertThat(testAction1.getId()).isEqualTo("Page1_testCollection1.testAction1"); + + assertThat(actionCollectionList.isEmpty()).isFalse(); + assertThat(actionCollectionList).hasSize(1); + final ActionCollection actionCollection = actionCollectionList.get(0); + assertThat(actionCollection.getApplicationId()).isNull(); + assertThat(actionCollection.getWorkspaceId()).isNull(); + assertThat(actionCollection.getPolicies()).isNull(); + assertThat(actionCollection.getId()).isNotNull(); + assertThat(actionCollection.getUnpublishedCollection().getPluginType()) + .isEqualTo(PluginType.JS); + assertThat(actionCollection.getUnpublishedCollection().getPageId()) + .isEqualTo(defaultPage.getUnpublishedPage().getName()); + assertThat(actionCollection.getUnpublishedCollection().getPluginId()) + .isEqualTo(installedJsPlugin.getPackageName()); + + assertThat(datasourceList).hasSize(1); + DatasourceStorage datasource = datasourceList.get(0); + assertThat(datasource.getWorkspaceId()).isNull(); + assertThat(datasource.getId()).isNull(); + assertThat(datasource.getPluginId()).isEqualTo(installedPlugin.getPackageName()); + assertThat(datasource.getDatasourceConfiguration()).isNotNull(); + + final Map invisibleActionFields = + applicationJson.getInvisibleActionFields(); + + assertThat(invisibleActionFields).isNull(); + for (NewAction newAction : actionList) { + if (newAction.getId().equals("Page1_validAction2")) { + assertEquals(true, newAction.getUnpublishedAction().getUserSetOnLoad()); + } else { + assertEquals(false, newAction.getUnpublishedAction().getUserSetOnLoad()); + } + } + + assertThat(applicationJson.getUnpublishedLayoutmongoEscapedWidgets()) + .isNull(); + assertThat(applicationJson.getPublishedLayoutmongoEscapedWidgets()) + .isNull(); + assertThat(applicationJson.getEditModeTheme()).isNotNull(); + assertThat(applicationJson.getEditModeTheme().isSystemTheme()) + .isTrue(); + assertThat(applicationJson.getEditModeTheme().getName()) + .isEqualToIgnoringCase(Theme.DEFAULT_THEME_NAME); + + assertThat(applicationJson.getPublishedTheme()).isNotNull(); + assertThat(applicationJson.getPublishedTheme().isSystemTheme()) + .isTrue(); + assertThat(applicationJson.getPublishedTheme().getName()) + .isEqualToIgnoringCase(Theme.DEFAULT_THEME_NAME); + + assertThat(exportedCollectionIds).isNotEmpty(); + assertThat(exportedCollectionIds).doesNotContain(String.valueOf(DBCollectionIds)); + + assertThat(exportedActionIds).isNotEmpty(); + assertThat(exportedActionIds).doesNotContain(String.valueOf(DBActionIds)); + + assertThat(exportedOnLayoutLoadActionIds).isNotEmpty(); + assertThat(exportedOnLayoutLoadActionIds).doesNotContain(String.valueOf(DBOnLayoutLoadActionIds)); + + assertThat(applicationJson.getDecryptedFields()).isNotNull(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void + importApplication_datasourceWithSameNameAndDifferentPlugin_importedWithValidActionsAndSuffixedDatasource() { + + ApplicationJson applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json") + .block(); + + Workspace testWorkspace = new Workspace(); + testWorkspace.setName("Duplicate datasource with different plugin org"); + testWorkspace = workspaceService.create(testWorkspace).block(); + String defaultEnvironmentId = workspaceService + .getDefaultEnvironmentId(testWorkspace.getId(), environmentPermission.getExecutePermission()) + .block(); + + Datasource testDatasource = new Datasource(); + // Chose any plugin except for mongo, as json static file has mongo plugin for datasource + Plugin postgreSQLPlugin = pluginRepository.findByName("PostgreSQL").block(); + testDatasource.setPluginId(postgreSQLPlugin.getId()); + testDatasource.setWorkspaceId(testWorkspace.getId()); + final String datasourceName = applicationJson.getDatasourceList().get(0).getName(); + testDatasource.setName(datasourceName); + + HashMap storages = new HashMap<>(); + storages.put(defaultEnvironmentId, new DatasourceStorageDTO(null, defaultEnvironmentId, null)); + testDatasource.setDatasourceStorages(storages); + + datasourceService.create(testDatasource).block(); + + final Mono resultMono = importService + .importNewArtifactInWorkspaceFromJson(testWorkspace.getId(), applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + + StepVerifier.create(resultMono.flatMap(application -> Mono.zip( + Mono.just(application), + datasourceService + .getAllByWorkspaceIdWithStorages( + application.getWorkspaceId(), Optional.of(MANAGE_DATASOURCES)) + .collectList(), + newActionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List datasourceList = tuple.getT2(); + final List actionList = tuple.getT3(); + + assertThat(application.getName()).isEqualTo("valid_application"); + + List datasourceNameList = new ArrayList<>(); + assertThat(datasourceList).isNotEmpty(); + datasourceList.forEach(datasource -> { + assertThat(datasource.getWorkspaceId()).isEqualTo(application.getWorkspaceId()); + datasourceNameList.add(datasource.getName()); + }); + // Check if both suffixed and newly imported datasource are present + assertThat(datasourceNameList).contains(datasourceName, datasourceName + " #1"); + + assertThat(actionList).isNotEmpty(); + actionList.forEach(newAction -> { + ActionDTO actionDTO = newAction.getUnpublishedAction(); + assertThat(actionDTO.getDatasource()).isNotNull(); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_datasourceWithSameNameAndPlugin_importedWithValidActionsWithoutSuffixedDatasource() { + + ApplicationJson applicationJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json") + .block(); + + Workspace testWorkspace = new Workspace(); + testWorkspace.setName("Duplicate datasource with same plugin org"); + testWorkspace = workspaceService.create(testWorkspace).block(); + String defaultEnvironmentId = workspaceService + .getDefaultEnvironmentId(testWorkspace.getId(), environmentPermission.getExecutePermission()) + .block(); + Datasource testDatasource = new Datasource(); + // Chose plugin same as mongo, as json static file has mongo plugin for datasource + Plugin postgreSQLPlugin = pluginRepository.findByName("MongoDB").block(); + testDatasource.setPluginId(postgreSQLPlugin.getId()); + testDatasource.setWorkspaceId(testWorkspace.getId()); + final String datasourceName = applicationJson.getDatasourceList().get(0).getName(); + testDatasource.setName(datasourceName); + + HashMap storages = new HashMap<>(); + storages.put(defaultEnvironmentId, new DatasourceStorageDTO(null, defaultEnvironmentId, null)); + testDatasource.setDatasourceStorages(storages); + datasourceService.create(testDatasource).block(); + + final Mono resultMono = importService + .importNewArtifactInWorkspaceFromJson(testWorkspace.getId(), applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + + StepVerifier.create(resultMono.flatMap(application -> Mono.zip( + Mono.just(application), + datasourceService + .getAllByWorkspaceIdWithStorages( + application.getWorkspaceId(), Optional.of(MANAGE_DATASOURCES)) + .collectList(), + newActionService + .findAllByApplicationIdAndViewMode(application.getId(), false, READ_ACTIONS, null) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List datasourceList = tuple.getT2(); + final List actionList = tuple.getT3(); + + assertThat(application.getName()).isEqualTo("valid_application"); + + List datasourceNameList = new ArrayList<>(); + assertThat(datasourceList).isNotEmpty(); + datasourceList.forEach(datasource -> { + assertThat(datasource.getWorkspaceId()).isEqualTo(application.getWorkspaceId()); + datasourceNameList.add(datasource.getName()); + }); + // Check that there are no datasources are created with suffix names as datasource's are of same + // plugin + assertThat(datasourceNameList).contains(datasourceName); + + assertThat(actionList).isNotEmpty(); + actionList.forEach(newAction -> { + ActionDTO actionDTO = newAction.getUnpublishedAction(); + assertThat(actionDTO.getDatasource()).isNotNull(); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void + exportAndImportApplication_withMultiplePagesOrderSameInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode() { + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("template-org-with-ds"); + + Application testApplication = new Application(); + testApplication.setName( + "exportAndImportApplication_withMultiplePagesOrderSameInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode"); + testApplication.setExportWithConfiguration(true); + testApplication = applicationPageService + .createApplication(testApplication, workspaceId) + .block(); + assert testApplication != null; + + PageDTO testPage1 = new PageDTO(); + testPage1.setName("testPage1"); + testPage1.setApplicationId(testApplication.getId()); + testPage1 = applicationPageService.createPage(testPage1).block(); + + PageDTO testPage2 = new PageDTO(); + testPage2.setName("testPage2"); + testPage2.setApplicationId(testApplication.getId()); + testPage2 = applicationPageService.createPage(testPage2).block(); + + // Set order for the newly created pages + applicationPageService + .reorderPage(testApplication.getId(), testPage1.getId(), 0, null) + .block(); + applicationPageService + .reorderPage(testApplication.getId(), testPage2.getId(), 1, null) + .block(); + // Deploy the current application + applicationPageService.publish(testApplication.getId(), true).block(); + + Mono applicationJsonMono = exportApplicationService + .exportApplicationById(testApplication.getId(), "") + .cache(); + + StepVerifier.create(applicationJsonMono) + .assertNext(applicationJson -> { + assertThat(applicationJson.getPageOrder()).isNull(); + assertThat(applicationJson.getPublishedPageOrder()).isNull(); + List pageList = applicationJson.getExportedApplication().getPages().stream() + .map(ApplicationPage::getId) + .collect(Collectors.toList()); + + assertThat(pageList.get(0)).isEqualTo("testPage1"); + assertThat(pageList.get(1)).isEqualTo("testPage2"); + assertThat(pageList.get(2)).isEqualTo("Page1"); + + List publishedPageList = + applicationJson.getExportedApplication().getPublishedPages().stream() + .map(ApplicationPage::getId) + .collect(Collectors.toList()); + + assertThat(publishedPageList.get(0)).isEqualTo("testPage1"); + assertThat(publishedPageList.get(1)).isEqualTo("testPage2"); + assertThat(publishedPageList.get(2)).isEqualTo("Page1"); + }) + .verifyComplete(); + + ApplicationJson applicationJson = applicationJsonMono.block(); + Application application = importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact) + .block(); + + // Get the unpublished pages and verify the order + List pageDTOS = application.getPages(); + Mono newPageMono1 = newPageService.findById(pageDTOS.get(0).getId(), MANAGE_PAGES); + Mono newPageMono2 = newPageService.findById(pageDTOS.get(1).getId(), MANAGE_PAGES); + Mono newPageMono3 = newPageService.findById(pageDTOS.get(2).getId(), MANAGE_PAGES); + + StepVerifier.create(Mono.zip(newPageMono1, newPageMono2, newPageMono3)) + .assertNext(objects -> { + NewPage newPage1 = objects.getT1(); + NewPage newPage2 = objects.getT2(); + NewPage newPage3 = objects.getT3(); + assertThat(newPage1.getUnpublishedPage().getName()).isEqualTo("testPage1"); + assertThat(newPage2.getUnpublishedPage().getName()).isEqualTo("testPage2"); + assertThat(newPage3.getUnpublishedPage().getName()).isEqualTo("Page1"); + + assertThat(newPage1.getId()).isEqualTo(pageDTOS.get(0).getId()); + assertThat(newPage2.getId()).isEqualTo(pageDTOS.get(1).getId()); + assertThat(newPage3.getId()).isEqualTo(pageDTOS.get(2).getId()); + }) + .verifyComplete(); + + // Get the published pages + List publishedPageDTOs = application.getPublishedPages(); + Mono newPublishedPageMono1 = + newPageService.findById(publishedPageDTOs.get(0).getId(), MANAGE_PAGES); + Mono newPublishedPageMono2 = + newPageService.findById(publishedPageDTOs.get(1).getId(), MANAGE_PAGES); + Mono newPublishedPageMono3 = + newPageService.findById(publishedPageDTOs.get(2).getId(), MANAGE_PAGES); + + StepVerifier.create(Mono.zip(newPublishedPageMono1, newPublishedPageMono2, newPublishedPageMono3)) + .assertNext(objects -> { + NewPage newPage1 = objects.getT1(); + NewPage newPage2 = objects.getT2(); + NewPage newPage3 = objects.getT3(); + assertThat(newPage1.getPublishedPage().getName()).isEqualTo("testPage1"); + assertThat(newPage2.getPublishedPage().getName()).isEqualTo("testPage2"); + assertThat(newPage3.getPublishedPage().getName()).isEqualTo("Page1"); + + assertThat(newPage1.getId()) + .isEqualTo(publishedPageDTOs.get(0).getId()); + assertThat(newPage2.getId()) + .isEqualTo(publishedPageDTOs.get(1).getId()); + assertThat(newPage3.getId()) + .isEqualTo(publishedPageDTOs.get(2).getId()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void + importApplicationInWorkspaceFromGit_WithNavSettingsInEditMode_ImportedAppHasNavSettingsInEditAndViewMode() { + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("import-with-navSettings-in-editMode"); + + Application testApplication = new Application(); + testApplication.setName( + "importApplicationInWorkspaceFromGit_WithNavSettingsInEditMode_ImportedAppHasNavSettingsInEditAndViewMode"); + Application.NavigationSetting appNavigationSetting = new Application.NavigationSetting(); + appNavigationSetting.setOrientation("top"); + testApplication.setUnpublishedApplicationDetail(new ApplicationDetail()); + testApplication.getUnpublishedApplicationDetail().setNavigationSetting(appNavigationSetting); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("testBranch"); + testApplication.setGitApplicationMetadata(gitData); + Application savedApplication = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + Mono result = exportApplicationService + .exportApplicationById(savedApplication.getId(), SerialiseApplicationObjective.VERSION_CONTROL) + .flatMap(applicationJson -> { + // setting published mode resource as null, similar to the app json exported to git repo + applicationJson.getExportedApplication().setPublishedApplicationDetail(null); + return importService + .importArtifactInWorkspaceFromGit( + workspaceId, savedApplication.getId(), applicationJson, gitData.getBranchName()) + .map(importableArtifact -> (Application) importableArtifact); + }); + + StepVerifier.create(result) + .assertNext(importedApp -> { + assertThat(importedApp.getUnpublishedApplicationDetail()).isNotNull(); + assertThat(importedApp.getPublishedApplicationDetail()).isNotNull(); + assertThat(importedApp.getUnpublishedApplicationDetail().getNavigationSetting()) + .isNotNull(); + assertEquals( + importedApp + .getUnpublishedApplicationDetail() + .getNavigationSetting() + .getOrientation(), + "top"); + assertThat(importedApp.getPublishedApplicationDetail().getNavigationSetting()) + .isNotNull(); + assertEquals( + importedApp + .getPublishedApplicationDetail() + .getNavigationSetting() + .getOrientation(), + "top"); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplicationInWorkspaceFromGit_WithAppLayoutInEditMode_ImportedAppHasAppLayoutInEditAndViewMode() { + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("import-with-appLayout-in-editMode"); + + Application testApplication = new Application(); + testApplication.setName( + "importApplicationInWorkspaceFromGit_WithAppLayoutInEditMode_ImportedAppHasAppLayoutInEditAndViewMode"); + testApplication.setUnpublishedAppLayout(new Application.AppLayout(Application.AppLayout.Type.DESKTOP)); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("testBranch"); + testApplication.setGitApplicationMetadata(gitData); + Application savedApplication = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + Mono result = exportApplicationService + .exportApplicationById(savedApplication.getId(), SerialiseApplicationObjective.VERSION_CONTROL) + .flatMap(applicationJson -> { + // setting published mode resource as null, similar to the app json exported to git repo + applicationJson.getExportedApplication().setPublishedAppLayout(null); + return importService + .importArtifactInWorkspaceFromGit( + workspaceId, savedApplication.getId(), applicationJson, gitData.getBranchName()) + .map(importableArtifact -> (Application) importableArtifact); + }); + + StepVerifier.create(result) + .assertNext(importedApp -> { + assertThat(importedApp.getUnpublishedAppLayout()).isNotNull(); + assertThat(importedApp.getPublishedAppLayout()).isNotNull(); + assertThat(importedApp.getUnpublishedAppLayout().getType()) + .isEqualTo(Application.AppLayout.Type.DESKTOP); + assertThat(importedApp.getPublishedAppLayout().getType()) + .isEqualTo(Application.AppLayout.Type.DESKTOP); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void + exportAndImportApplication_withMultiplePagesOrderDifferentInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode() { + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("template-org-with-ds"); + + Application testApplication = new Application(); + testApplication.setName( + "exportAndImportApplication_withMultiplePagesOrderDifferentInDeployAndEditMode_PagesOrderIsMaintainedInEditAndViewMode"); + testApplication.setExportWithConfiguration(true); + testApplication = applicationPageService + .createApplication(testApplication, workspaceId) + .block(); + assert testApplication != null; + + PageDTO testPage1 = new PageDTO(); + testPage1.setName("testPage1"); + testPage1.setApplicationId(testApplication.getId()); + testPage1 = applicationPageService.createPage(testPage1).block(); + + PageDTO testPage2 = new PageDTO(); + testPage2.setName("testPage2"); + testPage2.setApplicationId(testApplication.getId()); + testPage2 = applicationPageService.createPage(testPage2).block(); + + // Deploy the current application so that edit and view mode will have different page order + applicationPageService.publish(testApplication.getId(), true).block(); + + // Set order for the newly created pages + applicationPageService + .reorderPage(testApplication.getId(), testPage1.getId(), 0, null) + .block(); + applicationPageService + .reorderPage(testApplication.getId(), testPage2.getId(), 1, null) + .block(); + + Mono applicationJsonMono = exportApplicationService + .exportApplicationById(testApplication.getId(), "") + .cache(); + + StepVerifier.create(applicationJsonMono) + .assertNext(applicationJson -> { + Application exportedApplication = applicationJson.getExportedApplication(); + exportedApplication.setViewMode(false); + List pageOrder = exportedApplication.getPages().stream() + .map(ApplicationPage::getId) + .collect(Collectors.toList()); + assertThat(pageOrder.get(0)).isEqualTo("testPage1"); + assertThat(pageOrder.get(1)).isEqualTo("testPage2"); + assertThat(pageOrder.get(2)).isEqualTo("Page1"); + + pageOrder.clear(); + pageOrder = exportedApplication.getPublishedPages().stream() + .map(ApplicationPage::getId) + .collect(Collectors.toList()); + assertThat(pageOrder.get(0)).isEqualTo("Page1"); + assertThat(pageOrder.get(1)).isEqualTo("testPage1"); + assertThat(pageOrder.get(2)).isEqualTo("testPage2"); + }) + .verifyComplete(); + + ApplicationJson applicationJson = applicationJsonMono.block(); + Application application = importService + .importNewArtifactInWorkspaceFromJson(workspaceId, applicationJson) + .map(importableArtifact -> (Application) importableArtifact) + .block(); + + // Get the unpublished pages and verify the order + application.setViewMode(false); + List pageDTOS = application.getPages(); + Mono newPageMono1 = newPageService.findById(pageDTOS.get(0).getId(), MANAGE_PAGES); + Mono newPageMono2 = newPageService.findById(pageDTOS.get(1).getId(), MANAGE_PAGES); + Mono newPageMono3 = newPageService.findById(pageDTOS.get(2).getId(), MANAGE_PAGES); + + StepVerifier.create(Mono.zip(newPageMono1, newPageMono2, newPageMono3)) + .assertNext(objects -> { + NewPage newPage1 = objects.getT1(); + NewPage newPage2 = objects.getT2(); + NewPage newPage3 = objects.getT3(); + assertThat(newPage1.getUnpublishedPage().getName()).isEqualTo("testPage1"); + assertThat(newPage2.getUnpublishedPage().getName()).isEqualTo("testPage2"); + assertThat(newPage3.getUnpublishedPage().getName()).isEqualTo("Page1"); + + assertThat(newPage1.getId()).isEqualTo(pageDTOS.get(0).getId()); + assertThat(newPage2.getId()).isEqualTo(pageDTOS.get(1).getId()); + assertThat(newPage3.getId()).isEqualTo(pageDTOS.get(2).getId()); + }) + .verifyComplete(); + + // Get the published pages + List publishedPageDTOs = application.getPublishedPages(); + Mono newPublishedPageMono1 = + newPageService.findById(publishedPageDTOs.get(0).getId(), MANAGE_PAGES); + Mono newPublishedPageMono2 = + newPageService.findById(publishedPageDTOs.get(1).getId(), MANAGE_PAGES); + Mono newPublishedPageMono3 = + newPageService.findById(publishedPageDTOs.get(2).getId(), MANAGE_PAGES); + + StepVerifier.create(Mono.zip(newPublishedPageMono1, newPublishedPageMono2, newPublishedPageMono3)) + .assertNext(objects -> { + NewPage newPage1 = objects.getT1(); + NewPage newPage2 = objects.getT2(); + NewPage newPage3 = objects.getT3(); + assertThat(newPage1.getPublishedPage().getName()).isEqualTo("Page1"); + assertThat(newPage2.getPublishedPage().getName()).isEqualTo("testPage1"); + assertThat(newPage3.getPublishedPage().getName()).isEqualTo("testPage2"); + + assertThat(newPage1.getId()) + .isEqualTo(publishedPageDTOs.get(0).getId()); + assertThat(newPage2.getId()) + .isEqualTo(publishedPageDTOs.get(1).getId()); + assertThat(newPage3.getId()) + .isEqualTo(publishedPageDTOs.get(2).getId()); + }) + .verifyComplete(); + } + + private ApplicationJson createApplicationJSON(List pageNames) { + ApplicationJson applicationJson = new ApplicationJson(); + + // set the application data + Application application = new Application(); + application.setName("Template Application"); + application.setSlug("template-application"); + application.setForkingEnabled(true); + application.setIsPublic(true); + application.setApplicationVersion(ApplicationVersion.LATEST_VERSION); + applicationJson.setExportedApplication(application); + + DatasourceStorage sampleDatasource = new DatasourceStorage(); + sampleDatasource.setName("SampleDS"); + sampleDatasource.setPluginId("restapi-plugin"); + + applicationJson.setDatasourceList(List.of(sampleDatasource)); + + // add pages and actions + List newPageList = new ArrayList<>(pageNames.size()); + List actionList = new ArrayList<>(); + List actionCollectionList = new ArrayList<>(); + + for (String pageName : pageNames) { + NewPage newPage = new NewPage(); + newPage.setUnpublishedPage(new PageDTO()); + newPage.getUnpublishedPage().setName(pageName); + newPage.getUnpublishedPage().setLayouts(List.of()); + newPageList.add(newPage); + + NewAction action = new NewAction(); + action.setId(pageName + "_SampleQuery"); + action.setPluginType(PluginType.API); + action.setPluginId("restapi-plugin"); + action.setUnpublishedAction(new ActionDTO()); + action.getUnpublishedAction().setName("SampleQuery"); + action.getUnpublishedAction().setPageId(pageName); + action.getUnpublishedAction().setDatasource(new Datasource()); + action.getUnpublishedAction().getDatasource().setId("SampleDS"); + action.getUnpublishedAction().getDatasource().setPluginId("restapi-plugin"); + actionList.add(action); + + ActionCollection actionCollection = new ActionCollection(); + actionCollection.setId(pageName + "_SampleJS"); + actionCollection.setUnpublishedCollection(new ActionCollectionDTO()); + actionCollection.getUnpublishedCollection().setName("SampleJS"); + actionCollection.getUnpublishedCollection().setPageId(pageName); + actionCollection.getUnpublishedCollection().setPluginId("js-plugin"); + actionCollection.getUnpublishedCollection().setPluginType(PluginType.JS); + actionCollection.getUnpublishedCollection().setBody("export default {\\n\\t\\n}"); + actionCollectionList.add(actionCollection); + } + + applicationJson.setPageList(newPageList); + applicationJson.setActionList(actionList); + applicationJson.setActionCollectionList(actionCollectionList); + return applicationJson; + } + + @Test + @WithUserDetails("api_user") + public void mergeApplicationJsonWithApplication_WhenPageNameConflicts_PageNamesRenamed() { + String uniqueString = UUID.randomUUID().toString(); + + Application destApplication = new Application(); + destApplication.setName("App_" + uniqueString); + destApplication.setSlug("my-slug"); + destApplication.setIsPublic(false); + destApplication.setForkingEnabled(false); + Mono createAppAndPageMono = applicationPageService + .createApplication(destApplication, workspaceId) + .flatMap(application -> { + PageDTO pageDTO = new PageDTO(); + pageDTO.setName("Home"); + pageDTO.setApplicationId(application.getId()); + return applicationPageService.createPage(pageDTO).thenReturn(application); + }); + + // let's create an ApplicationJSON which we'll merge with application created by createAppAndPageMono + ApplicationJson applicationJson = createApplicationJSON(List.of("Home", "About")); + + Mono, List>> tuple2Mono = createAppAndPageMono + .flatMap(application -> + // merge the application json with the application we've created + importService + .mergeArtifactExchangeJsonWithImportableArtifact( + application.getWorkspaceId(), application.getId(), null, applicationJson, null) + .thenReturn(application)) + .flatMap(application -> + // fetch the application pages, this should contain pages from application json + Mono.zip( + newPageService.findApplicationPages( + application.getId(), null, null, ApplicationMode.EDIT), + newActionService + .findAllByApplicationIdAndViewMode( + application.getId(), false, MANAGE_ACTIONS, null) + .collectList(), + actionCollectionService + .findAllByApplicationIdAndViewMode( + application.getId(), false, MANAGE_ACTIONS, null) + .collectList())); + + StepVerifier.create(tuple2Mono) + .assertNext(objects -> { + ApplicationPagesDTO applicationPagesDTO = objects.getT1(); + List newActionList = objects.getT2(); + List actionCollectionList = objects.getT3(); + + assertThat(applicationPagesDTO.getApplication().getName()).isEqualTo(destApplication.getName()); + assertThat(applicationPagesDTO.getApplication().getSlug()).isEqualTo(destApplication.getSlug()); + assertThat(applicationPagesDTO.getApplication().getIsPublic()) + .isFalse(); + assertThat(applicationPagesDTO.getApplication().getForkingEnabled()) + .isFalse(); + assertThat(applicationPagesDTO.getPages().size()).isEqualTo(4); + List pageNames = applicationPagesDTO.getPages().stream() + .map(PageNameIdDTO::getName) + .collect(Collectors.toList()); + assertThat(pageNames).contains("Home", "Home2", "About"); + assertThat(newActionList.size()).isEqualTo(2); // we imported two pages and each page has one action + assertThat(actionCollectionList.size()) + .isEqualTo(2); // we imported two pages and each page has one Collection + }) + .verifyComplete(); + } + + @Test + @WithUserDetails("api_user") + public void mergeApplicationJsonWithApplication_WhenPageListIProvided_OnlyListedPagesAreMerged() { + String uniqueString = UUID.randomUUID().toString(); + + Application destApplication = new Application(); + destApplication.setName("App_" + uniqueString); + Mono createAppAndPageMono = applicationPageService + .createApplication(destApplication, workspaceId) + .flatMap(application -> { + PageDTO pageDTO = new PageDTO(); + pageDTO.setName("Home"); + pageDTO.setApplicationId(application.getId()); + return applicationPageService.createPage(pageDTO).thenReturn(application); + }); + + // let's create an ApplicationJSON which we'll merge with application created by createAppAndPageMono + ApplicationJson applicationJson = createApplicationJSON(List.of("Profile", "About", "Contact US")); + + Mono applicationPagesDTOMono = createAppAndPageMono + .flatMap(application -> + // merge the application json with the application we've created + importService + .mergeArtifactExchangeJsonWithImportableArtifact( + application.getWorkspaceId(), + application.getId(), + null, + applicationJson, + List.of("About", "Contact US")) + .thenReturn(application)) + .flatMap(application -> + // fetch the application pages, this should contain pages from application json + newPageService.findApplicationPages(application.getId(), null, null, ApplicationMode.EDIT)); + + StepVerifier.create(applicationPagesDTOMono) + .assertNext(applicationPagesDTO -> { + assertThat(applicationPagesDTO.getPages().size()).isEqualTo(4); + List pageNames = applicationPagesDTO.getPages().stream() + .map(PageNameIdDTO::getName) + .collect(Collectors.toList()); + assertThat(pageNames).contains("Home", "About", "Contact US"); + assertThat(pageNames).doesNotContain("Profile"); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void exportApplicationById_WhenThemeDoesNotExist_ExportedWithDefaultTheme() { + Theme customTheme = new Theme(); + customTheme.setName("my-custom-theme"); + + String randomId = UUID.randomUUID().toString(); + Application testApplication = new Application(); + testApplication.setName("Application_" + randomId); + Mono exportedAppJson = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application -> { + application.setEditModeThemeId("invalid-theme-id"); + application.setPublishedModeThemeId("invalid-theme-id"); + String branchName = null; + return applicationService + .save(application) + .then(exportApplicationService.exportApplicationById(application.getId(), branchName)); + }); + + StepVerifier.create(exportedAppJson) + .assertNext(applicationJson -> { + assertThat(applicationJson.getEditModeTheme().getName()) + .isEqualToIgnoringCase(Theme.DEFAULT_THEME_NAME); + assertThat(applicationJson.getPublishedTheme().getName()) + .isEqualToIgnoringCase(Theme.DEFAULT_THEME_NAME); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_invalidPluginReferenceForDatasource_throwException() { + Mockito.when(pluginService.findAllByIdsWithoutPermission(Mockito.any(), Mockito.anyList())) + .thenReturn(Flux.fromIterable(List.of(installedPlugin, installedJsPlugin))); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + + ApplicationJson appJson = createAppJson("test_assets/ImportExportServiceTest/valid-application.json") + .block(); + assert appJson != null; + final String randomId = UUID.randomUUID().toString(); + appJson.getDatasourceList().get(0).setPluginId(randomId); + Workspace createdWorkspace = workspaceService.create(newWorkspace).block(); + final Mono resultMono = importService + .importNewArtifactInWorkspaceFromJson(createdWorkspace.getId(), appJson) + .map(importableArtifact -> (Application) importableArtifact); + + StepVerifier.create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException + && throwable + .getMessage() + .contains(AppsmithError.GENERIC_JSON_IMPORT_ERROR.getMessage( + createdWorkspace.getId(), ""))) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_importSameApplicationTwice_applicationImportedLaterWithSuffixCount() { + + Mono applicationJsonMono = + createAppJson("test_assets/ImportExportServiceTest/valid-application-without-action-collection.json"); + + Workspace newWorkspace = new Workspace(); + newWorkspace.setName("Template Workspace"); + + Mono createWorkspaceMono = + workspaceService.create(newWorkspace).cache(); + final Mono importApplicationMono = createWorkspaceMono + .zipWith(applicationJsonMono) + .flatMap(tuple -> { + Workspace workspace = tuple.getT1(); + ApplicationJson applicationJson = tuple.getT2(); + return importService + .importNewArtifactInWorkspaceFromJson(workspace.getId(), applicationJson) + .map(importableArtifact -> (Application) importableArtifact); + }); + + StepVerifier.create(importApplicationMono.zipWhen(application -> importApplicationMono)) + .assertNext(tuple -> { + Application firstImportedApplication = tuple.getT1(); + Application secondImportedApplication = tuple.getT2(); + assertThat(firstImportedApplication.getName()).isEqualTo("valid_application"); + assertThat(secondImportedApplication.getName()).isEqualTo("valid_application (1)"); + assertThat(firstImportedApplication.getWorkspaceId()) + .isEqualTo(secondImportedApplication.getWorkspaceId()); + assertThat(firstImportedApplication.getWorkspaceId()).isNotNull(); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void mergeApplication_existingApplication_pageAddedSuccessfully() { + + // Create application + Application application = new Application(); + application.setName("mergeApplication_existingApplication_pageAddedSuccessfully"); + application.setWorkspaceId(workspaceId); + application = applicationPageService.createApplication(application).block(); + + Mono applicationJson = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + + Application finalApplication = application; + Mono, List, List>> importedApplication = + applicationJson + .flatMap(applicationJson1 -> importService.mergeArtifactExchangeJsonWithImportableArtifact( + workspaceId, finalApplication.getId(), null, applicationJson1, new ArrayList<>())) + .map(importableArtifact -> (Application) importableArtifact) + .flatMap(application1 -> { + Mono> pageList = newPageService + .findNewPagesByApplicationId(application1.getId(), MANAGE_PAGES) + .collectList(); + Mono> actionList = newActionService + .findAllByApplicationIdAndViewMode( + application1.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + Mono> actionCollectionList = actionCollectionService + .findAllByApplicationIdAndViewMode( + application1.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + return Mono.zip(Mono.just(application1), pageList, actionList, actionCollectionList); + }); + + StepVerifier.create(importedApplication) + .assertNext(tuple -> { + Application application1 = tuple.getT1(); + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + List actionCollectionList = tuple.getT4(); + + assertThat(application1.getId()).isEqualTo(finalApplication.getId()); + assertThat(finalApplication.getPages().size()) + .isLessThan(application1.getPages().size()); + assertThat(finalApplication.getPages().size()) + .isEqualTo(application1.getPublishedPages().size()); + + // Verify the pages after merging the template + pageList.forEach(newPage -> { + assertThat(newPage.getUnpublishedPage().getName()).containsAnyOf("Page1", "Page12", "Page2"); + assertThat(newPage.getGitSyncId()).isNotNull(); + }); + + NewPage page = pageList.stream() + .filter(newPage -> + newPage.getUnpublishedPage().getName().equals("Page12")) + .collect(Collectors.toList()) + .get(0); + // Verify the actions after merging the template + actionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedAction().getName()) + .containsAnyOf("api_wo_auth", "get_users", "run"); + assertThat(newAction.getUnpublishedAction().getPageId()).isEqualTo(page.getId()); + }); + + // Verify the actionCollections after merging the template + actionCollectionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedCollection().getName()) + .containsAnyOf("JSObject1", "JSObject2"); + assertThat(newAction.getUnpublishedCollection().getPageId()) + .isEqualTo(page.getId()); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void mergeApplication_gitConnectedApplication_pageAddedSuccessfully() { + + // Create application connected to git + Application testApplication = new Application(); + testApplication.setName("mergeApplication_gitConnectedApplication_pageAddedSuccessfully"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("master"); + gitData.setDefaultBranchName("master"); + testApplication.setGitApplicationMetadata(gitData); + + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + Mono applicationJson = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + + Application finalApplication = application; + Mono, List, List>> importedApplication = + applicationJson + .flatMap(applicationJson1 -> importService.mergeArtifactExchangeJsonWithImportableArtifact( + workspaceId, finalApplication.getId(), "master", applicationJson1, new ArrayList<>())) + .map(importableArtifact -> (Application) importableArtifact) + .flatMap(application1 -> { + Mono> pageList = newPageService + .findNewPagesByApplicationId(application1.getId(), MANAGE_PAGES) + .collectList(); + Mono> actionList = newActionService + .findAllByApplicationIdAndViewMode( + application1.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + Mono> actionCollectionList = actionCollectionService + .findAllByApplicationIdAndViewMode( + application1.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + return Mono.zip(Mono.just(application1), pageList, actionList, actionCollectionList); + }); + + StepVerifier.create(importedApplication) + .assertNext(tuple -> { + Application application1 = tuple.getT1(); + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + List actionCollectionList = tuple.getT4(); + + assertThat(application1.getId()).isEqualTo(finalApplication.getId()); + assertThat(finalApplication.getPages().size()) + .isLessThan(application1.getPages().size()); + assertThat(finalApplication.getPages().size()) + .isEqualTo(application1.getPublishedPages().size()); + + // Verify the pages after merging the template + pageList.forEach(newPage -> { + assertThat(newPage.getUnpublishedPage().getName()).containsAnyOf("Page1", "Page12", "Page2"); + assertThat(newPage.getGitSyncId()).isNotNull(); + }); + + NewPage page = pageList.stream() + .filter(newPage -> + newPage.getUnpublishedPage().getName().equals("Page12")) + .collect(Collectors.toList()) + .get(0); + // Verify the actions after merging the template + actionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedAction().getName()) + .containsAnyOf("api_wo_auth", "get_users", "run"); + assertThat(newAction.getUnpublishedAction().getPageId()).isEqualTo(page.getId()); + }); + + // Verify the actionCollections after merging the template + actionCollectionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedCollection().getName()) + .containsAnyOf("JSObject1", "JSObject2"); + assertThat(newAction.getUnpublishedCollection().getPageId()) + .isEqualTo(page.getId()); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void mergeApplication_gitConnectedApplicationChildBranch_pageAddedSuccessfully() { + + // Create application connected to git + Application testApplication = new Application(); + testApplication.setName("mergeApplication_gitConnectedApplicationChildBranch_pageAddedSuccessfully"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("master"); + gitData.setDefaultBranchName("master"); + testApplication.setGitApplicationMetadata(gitData); + + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + // Create branch for the application + testApplication = new Application(); + testApplication.setName("mergeApplication_gitConnectedApplicationChildBranch_pageAddedSuccessfully1"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData1 = new GitApplicationMetadata(); + gitData1.setBranchName("feature"); + gitData1.setDefaultBranchName("master"); + testApplication.setGitApplicationMetadata(gitData1); + + Application branchApp = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application2 -> { + application2.getGitApplicationMetadata().setDefaultApplicationId(application.getId()); + return applicationService.save(application2); + }) + .block(); + + Mono applicationJson = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + + Application finalApplication = application; + Mono, List, List>> importedApplication = + applicationJson + .flatMap(applicationJson1 -> importService.mergeArtifactExchangeJsonWithImportableArtifact( + workspaceId, branchApp.getId(), "feature", applicationJson1, new ArrayList<>())) + .map(importableArtifact -> (Application) importableArtifact) + .flatMap(application2 -> { + Mono> pageList = newPageService + .findNewPagesByApplicationId(branchApp.getId(), MANAGE_PAGES) + .collectList(); + Mono> actionList = newActionService + .findAllByApplicationIdAndViewMode(branchApp.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + Mono> actionCollectionList = actionCollectionService + .findAllByApplicationIdAndViewMode(branchApp.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + return Mono.zip(Mono.just(application2), pageList, actionList, actionCollectionList); + }); + + StepVerifier.create(importedApplication) + .assertNext(tuple -> { + Application application3 = tuple.getT1(); + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + List actionCollectionList = tuple.getT4(); + + assertThat(application3.getId()).isNotEqualTo(finalApplication.getId()); + assertThat(finalApplication.getPages().size()) + .isLessThan(application3.getPages().size()); + assertThat(finalApplication.getPages().size()) + .isEqualTo(application3.getPublishedPages().size()); + + // Verify the pages after merging the template + pageList.forEach(newPage -> { + assertThat(newPage.getUnpublishedPage().getName()).containsAnyOf("Page1", "Page12", "Page2"); + assertThat(newPage.getGitSyncId()).isNotNull(); + }); + + NewPage page = pageList.stream() + .filter(newPage -> + newPage.getUnpublishedPage().getName().equals("Page12")) + .collect(Collectors.toList()) + .get(0); + // Verify the actions after merging the template + actionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedAction().getName()) + .containsAnyOf("api_wo_auth", "get_users", "run"); + assertThat(newAction.getUnpublishedAction().getPageId()).isEqualTo(page.getId()); + }); + + // Verify the actionCollections after merging the template + actionCollectionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedCollection().getName()) + .containsAnyOf("JSObject1", "JSObject2"); + assertThat(newAction.getUnpublishedCollection().getPageId()) + .isEqualTo(page.getId()); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void mergeApplication_gitConnectedApplicationSelectedSpecificPages_selectedPageAddedSuccessfully() { + // Create application connected to git + Application testApplication = new Application(); + testApplication.setName( + "mergeApplication_gitConnectedApplicationSelectedSpecificPages_selectedPageAddedSuccessfully"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("master"); + gitData.setDefaultBranchName("master"); + testApplication.setGitApplicationMetadata(gitData); + + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + // Create branch for the application + testApplication = new Application(); + testApplication.setName( + "mergeApplication_gitConnectedApplicationSelectedSpecificPages_selectedPageAddedSuccessfully1"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData1 = new GitApplicationMetadata(); + gitData1.setBranchName("feature"); + gitData1.setDefaultBranchName("master"); + testApplication.setGitApplicationMetadata(gitData1); + + Application branchApp = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application2 -> { + application2.getGitApplicationMetadata().setDefaultApplicationId(application.getId()); + return applicationService.save(application2); + }) + .block(); + + Mono applicationJson = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + + Application finalApplication = application; + Mono, List, List>> importedApplication = + applicationJson + .flatMap(applicationJson1 -> importService.mergeArtifactExchangeJsonWithImportableArtifact( + workspaceId, branchApp.getId(), "feature", applicationJson1, List.of("Page1"))) + .map(importableArtifact -> (Application) importableArtifact) + .flatMap(application2 -> { + Mono> pageList = newPageService + .findNewPagesByApplicationId(branchApp.getId(), MANAGE_PAGES) + .collectList(); + Mono> actionList = newActionService + .findAllByApplicationIdAndViewMode(branchApp.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + Mono> actionCollectionList = actionCollectionService + .findAllByApplicationIdAndViewMode(branchApp.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + return Mono.zip(Mono.just(application2), pageList, actionList, actionCollectionList); + }); + + StepVerifier.create(importedApplication) + .assertNext(tuple -> { + Application application3 = tuple.getT1(); + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + List actionCollectionList = tuple.getT4(); + + assertThat(application3.getId()).isNotEqualTo(finalApplication.getId()); + assertThat(finalApplication.getPages().size()) + .isLessThan(application3.getPages().size()); + assertThat(finalApplication.getPages().size()) + .isEqualTo(application3.getPublishedPages().size()); + + // Verify the pages after merging the template + pageList.forEach(newPage -> { + assertThat(newPage.getUnpublishedPage().getName()).containsAnyOf("Page1", "Page12"); + assertThat(newPage.getGitSyncId()).isNotNull(); + }); + + NewPage page = pageList.stream() + .filter(newPage -> + newPage.getUnpublishedPage().getName().equals("Page12")) + .collect(Collectors.toList()) + .get(0); + // Verify the actions after merging the template + actionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedAction().getName()) + .containsAnyOf("api_wo_auth", "get_users", "run"); + assertThat(newAction.getUnpublishedAction().getPageId()).isEqualTo(page.getId()); + }); + + // Verify the actionCollections after merging the template + actionCollectionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedCollection().getName()) + .containsAnyOf("JSObject1", "JSObject2"); + assertThat(newAction.getUnpublishedCollection().getPageId()) + .isEqualTo(page.getId()); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void mergeApplication_gitConnectedApplicationSelectedAllPages_selectedPageAddedSuccessfully() { + // Create application connected to git + Application testApplication = new Application(); + testApplication.setName( + "mergeApplication_gitConnectedApplicationSelectedAllPages_selectedPageAddedSuccessfully"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setBranchName("master"); + gitData.setDefaultBranchName("master"); + testApplication.setGitApplicationMetadata(gitData); + + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + // Create branch for the application + testApplication = new Application(); + testApplication.setName( + "mergeApplication_gitConnectedApplicationSelectedAllPages_selectedPageAddedSuccessfully1"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setModifiedBy("some-user"); + testApplication.setGitApplicationMetadata(new GitApplicationMetadata()); + GitApplicationMetadata gitData1 = new GitApplicationMetadata(); + gitData1.setBranchName("feature"); + gitData1.setDefaultBranchName("master"); + testApplication.setGitApplicationMetadata(gitData1); + + Application branchApp = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application2 -> { + application2.getGitApplicationMetadata().setDefaultApplicationId(application.getId()); + return applicationService.save(application2); + }) + .block(); + + Mono applicationJson = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + + Application finalApplication = application; + Mono, List, List>> importedApplication = + applicationJson + .flatMap(applicationJson1 -> importService.mergeArtifactExchangeJsonWithImportableArtifact( + workspaceId, branchApp.getId(), "feature", applicationJson1, List.of("Page1", "Page2"))) + .map(importableArtifact -> (Application) importableArtifact) + .flatMap(application2 -> { + Mono> pageList = newPageService + .findNewPagesByApplicationId(branchApp.getId(), MANAGE_PAGES) + .collectList(); + Mono> actionList = newActionService + .findAllByApplicationIdAndViewMode(branchApp.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + Mono> actionCollectionList = actionCollectionService + .findAllByApplicationIdAndViewMode(branchApp.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + return Mono.zip(Mono.just(application2), pageList, actionList, actionCollectionList); + }); + + StepVerifier.create(importedApplication) + .assertNext(tuple -> { + Application application3 = tuple.getT1(); + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + List actionCollectionList = tuple.getT4(); + + assertThat(application3.getId()).isNotEqualTo(finalApplication.getId()); + assertThat(finalApplication.getPages().size()) + .isLessThan(application3.getPages().size()); + assertThat(finalApplication.getPages().size()) + .isEqualTo(application3.getPublishedPages().size()); + + // Verify the pages after merging the template + pageList.forEach(newPage -> { + assertThat(newPage.getUnpublishedPage().getName()).containsAnyOf("Page1", "Page12", "Page2"); + assertThat(newPage.getGitSyncId()).isNotNull(); + }); + + NewPage page = pageList.stream() + .filter(newPage -> + newPage.getUnpublishedPage().getName().equals("Page12")) + .collect(Collectors.toList()) + .get(0); + // Verify the actions after merging the template + actionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedAction().getName()) + .containsAnyOf("api_wo_auth", "get_users", "run"); + assertThat(newAction.getUnpublishedAction().getPageId()).isEqualTo(page.getId()); + }); + + // Verify the actionCollections after merging the template + actionCollectionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedCollection().getName()) + .containsAnyOf("JSObject1", "JSObject2"); + assertThat(newAction.getUnpublishedCollection().getPageId()) + .isEqualTo(page.getId()); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void mergeApplication_nonGitConnectedApplicationSelectedSpecificPages_selectedPageAddedSuccessfully() { + // Create application + Application application = new Application(); + application.setName( + "mergeApplication_nonGitConnectedApplicationSelectedSpecificPages_selectedPageAddedSuccessfully"); + application.setWorkspaceId(workspaceId); + application = applicationPageService.createApplication(application).block(); + + Mono applicationJson = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + + Application finalApplication = application; + Mono, List, List>> importedApplication = + applicationJson + .flatMap(applicationJson1 -> importService.mergeArtifactExchangeJsonWithImportableArtifact( + workspaceId, finalApplication.getId(), null, applicationJson1, List.of("Page1"))) + .map(importableArtifact -> (Application) importableArtifact) + .flatMap(application1 -> { + Mono> pageList = newPageService + .findNewPagesByApplicationId(application1.getId(), MANAGE_PAGES) + .collectList(); + Mono> actionList = newActionService + .findAllByApplicationIdAndViewMode( + application1.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + Mono> actionCollectionList = actionCollectionService + .findAllByApplicationIdAndViewMode( + application1.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + return Mono.zip(Mono.just(application1), pageList, actionList, actionCollectionList); + }); + + StepVerifier.create(importedApplication) + .assertNext(tuple -> { + Application application1 = tuple.getT1(); + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + List actionCollectionList = tuple.getT4(); + + assertThat(application1.getId()).isEqualTo(finalApplication.getId()); + assertThat(finalApplication.getPages().size()) + .isLessThan(application1.getPages().size()); + assertThat(finalApplication.getPages().size()) + .isEqualTo(application1.getPublishedPages().size()); + + // Verify the pages after merging the template + pageList.forEach(newPage -> { + assertThat(newPage.getUnpublishedPage().getName()).containsAnyOf("Page1", "Page12"); + assertThat(newPage.getGitSyncId()).isNotNull(); + }); + + NewPage page = pageList.stream() + .filter(newPage -> + newPage.getUnpublishedPage().getName().equals("Page12")) + .collect(Collectors.toList()) + .get(0); + // Verify the actions after merging the template + actionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedAction().getName()) + .containsAnyOf("api_wo_auth", "get_users", "run"); + assertThat(newAction.getUnpublishedAction().getPageId()).isEqualTo(page.getId()); + }); + + // Verify the actionCollections after merging the template + actionCollectionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedCollection().getName()) + .containsAnyOf("JSObject1", "JSObject2"); + assertThat(newAction.getUnpublishedCollection().getPageId()) + .isEqualTo(page.getId()); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void mergeApplication_nonGitConnectedApplicationSelectedAllPages_selectedPageAddedSuccessfully() { + // Create application + Application application = new Application(); + application.setName( + "mergeApplication_nonGitConnectedApplicationSelectedAllPages_selectedPageAddedSuccessfully"); + application.setWorkspaceId(workspaceId); + application = applicationPageService.createApplication(application).block(); + + Mono applicationJson = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + + Application finalApplication = application; + Mono, List, List>> importedApplication = + applicationJson + .flatMap(applicationJson1 -> importService.mergeArtifactExchangeJsonWithImportableArtifact( + workspaceId, + finalApplication.getId(), + null, + applicationJson1, + List.of("Page1", "Page2"))) + .map(importableArtifact -> (Application) importableArtifact) + .flatMap(application1 -> { + Mono> pageList = newPageService + .findNewPagesByApplicationId(application1.getId(), MANAGE_PAGES) + .collectList(); + Mono> actionList = newActionService + .findAllByApplicationIdAndViewMode( + application1.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + Mono> actionCollectionList = actionCollectionService + .findAllByApplicationIdAndViewMode( + application1.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + return Mono.zip(Mono.just(application1), pageList, actionList, actionCollectionList); + }); + + StepVerifier.create(importedApplication) + .assertNext(tuple -> { + Application application1 = tuple.getT1(); + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + List actionCollectionList = tuple.getT4(); + + assertThat(application1.getId()).isEqualTo(finalApplication.getId()); + assertThat(finalApplication.getPages().size()) + .isLessThan(application1.getPages().size()); + assertThat(finalApplication.getPages().size()) + .isEqualTo(application1.getPublishedPages().size()); + + // Verify the pages after merging the template + pageList.forEach(newPage -> { + assertThat(newPage.getUnpublishedPage().getName()).containsAnyOf("Page1", "Page12", "Page2"); + assertThat(newPage.getGitSyncId()).isNotNull(); + }); + + NewPage page = pageList.stream() + .filter(newPage -> + newPage.getUnpublishedPage().getName().equals("Page12")) + .collect(Collectors.toList()) + .get(0); + // Verify the actions after merging the template + actionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedAction().getName()) + .containsAnyOf("api_wo_auth", "get_users", "run"); + assertThat(newAction.getUnpublishedAction().getPageId()).isEqualTo(page.getId()); + }); + + // Verify the actionCollections after merging the template + actionCollectionList.forEach(newAction -> { + assertThat(newAction.getUnpublishedCollection().getName()) + .containsAnyOf("JSObject1", "JSObject2"); + assertThat(newAction.getUnpublishedCollection().getPageId()) + .isEqualTo(page.getId()); + }); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_invalidJson_createdAppIsDeleted() { + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/invalid-json-without-pages.json"); + + List applicationList = applicationService + .findAllApplicationsByWorkspaceId(workspaceId) + .collectList() + .block(); + + Mono resultMono = importService + .extractArtifactExchangeJsonAndSaveArtifact(filePart, workspaceId, null, ArtifactJsonType.APPLICATION) + .map(artifactImportDTO -> (ApplicationImportDTO) artifactImportDTO); + + StepVerifier.create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException + && throwable + .getMessage() + .equals(AppsmithError.VALIDATION_FAILURE.getMessage( + "Field '" + FieldName.PAGE_LIST + "' is missing in the JSON."))) + .verify(); + + // Verify that the app card is not created + StepVerifier.create(applicationService + .findAllApplicationsByWorkspaceId(workspaceId) + .collectList()) + .assertNext(applications -> { + assertThat(applicationList.size()).isEqualTo(applications.size()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void exportApplication_WithBearerTokenAndExportWithConfig_exportedWithDecryptedFields() { + String randomUUID = UUID.randomUUID().toString(); + + Workspace testWorkspace = new Workspace(); + testWorkspace.setName("workspace-" + randomUUID); + Workspace workspace = workspaceService.create(testWorkspace).block(); + + Application testApplication = new Application(); + testApplication.setName("application-" + randomUUID); + testApplication.setExportWithConfiguration(true); + testApplication.setWorkspaceId(workspace.getId()); + + Mono applicationMono = applicationPageService + .createApplication(testApplication) + .flatMap(application -> { + ApplicationAccessDTO accessDTO = new ApplicationAccessDTO(); + accessDTO.setPublicAccess(true); + return applicationService + .changeViewAccess(application.getId(), accessDTO) + .thenReturn(application); + }); + + Mono datasourceMono = workspaceService + .getDefaultEnvironmentId(workspace.getId(), environmentPermission.getExecutePermission()) + .zipWith(pluginRepository.findByPackageName("restapi-plugin")) + .flatMap(objects -> { + String defaultEnvironmentId = objects.getT1(); + Plugin plugin = objects.getT2(); + + Datasource datasource = new Datasource(); + datasource.setPluginId(plugin.getId()); + datasource.setName("RestAPIWithBearerToken"); + datasource.setWorkspaceId(workspace.getId()); + datasource.setIsConfigured(true); + + BearerTokenAuth bearerTokenAuth = new BearerTokenAuth(); + bearerTokenAuth.setBearerToken("token_" + randomUUID); + + SSLDetails sslDetails = new SSLDetails(); + sslDetails.setAuthType(SSLDetails.AuthType.DEFAULT); + Connection connection = new Connection(); + connection.setSsl(sslDetails); + + DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setAuthentication(bearerTokenAuth); + datasourceConfiguration.setConnection(connection); + datasourceConfiguration.setConnection(new Connection()); + datasourceConfiguration.setUrl("https://mock-api.appsmith.com"); + + HashMap storages = new HashMap<>(); + storages.put( + defaultEnvironmentId, + new DatasourceStorageDTO(null, defaultEnvironmentId, datasourceConfiguration)); + datasource.setDatasourceStorages(storages); + + return datasourceService.create(datasource); + }); + + Mono exportAppMono = Mono.zip(applicationMono, datasourceMono) + .flatMap(objects -> { + ApplicationPage applicationPage = objects.getT1().getPages().get(0); + ActionDTO action = new ActionDTO(); + action.setName("validAction"); + action.setPageId(applicationPage.getId()); + action.setPluginId(objects.getT2().getPluginId()); + action.setDatasource(objects.getT2()); + + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + actionConfiguration.setPath("/test/path"); + action.setActionConfiguration(actionConfiguration); + + return layoutActionService + .createSingleAction(action, Boolean.FALSE) + .then(exportApplicationService.exportApplicationById( + objects.getT1().getId(), "")); + }); + + StepVerifier.create(exportAppMono) + .assertNext(applicationJson -> { + assertThat(applicationJson.getDecryptedFields()).isNotEmpty(); + DecryptedSensitiveFields fields = + applicationJson.getDecryptedFields().get("RestAPIWithBearerToken"); + assertThat(fields.getBearerTokenAuth().getBearerToken()).isEqualTo("token_" + randomUUID); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void exportApplicationTest_WithNavigationSettings() { + + Application application = new Application(); + application.setName("exportNavigationSettingsApplicationTest"); + Application.NavigationSetting navSetting = new Application.NavigationSetting(); + navSetting.setOrientation("top"); + application.setUnpublishedApplicationDetail(new ApplicationDetail()); + application.getUnpublishedApplicationDetail().setNavigationSetting(navSetting); + Application createdApplication = applicationPageService + .createApplication(application, workspaceId) + .block(); + + Mono resultMono = + exportApplicationService.exportApplicationById(createdApplication.getId(), ""); + + StepVerifier.create(resultMono) + .assertNext(applicationJson -> { + Application exportedApplication = applicationJson.getExportedApplication(); + assertThat(exportedApplication).isNotNull(); + assertThat(exportedApplication + .getUnpublishedApplicationDetail() + .getNavigationSetting()) + .isNotNull(); + assertThat(exportedApplication + .getUnpublishedApplicationDetail() + .getNavigationSetting() + .getOrientation()) + .isEqualTo("top"); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void exportApplication_WithPageIcon_ValidPageIcon() { + String randomId = UUID.randomUUID().toString(); + Application application = new Application(); + application.setName("exportPageIconApplicationTest"); + Application createdApplication = applicationPageService + .createApplication(application, workspaceId) + .block(); + + PageDTO pageDTO = new PageDTO(); + pageDTO.setName("page_" + randomId); + pageDTO.setIcon("flight"); + pageDTO.setApplicationId(createdApplication.getId()); + + PageDTO applicationPageDTO = applicationPageService.createPage(pageDTO).block(); + + Mono resultMono = + exportApplicationService.exportApplicationById(applicationPageDTO.getApplicationId(), ""); + + StepVerifier.create(resultMono) + .assertNext(applicationJson -> { + List pages = applicationJson.getPageList(); + assertThat(pages.size()).isEqualTo(2); + assertThat(pages.get(1).getUnpublishedPage().getName()).isEqualTo("page_" + randomId); + assertThat(pages.get(1).getUnpublishedPage().getIcon()).isEqualTo("flight"); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void importApplication_existingApplication_ApplicationReplacedWithImportedOne() { + String randomUUID = UUID.randomUUID().toString(); + Mono applicationJson = + createAppJson("test_assets/ImportExportServiceTest/valid-application.json"); + + // Create the initial application + Application application = new Application(); + application.setName("Application_" + randomUUID); + application.setWorkspaceId(workspaceId); + + Mono, List, List>> importedApplication = + applicationPageService + .createApplication(application) + .flatMap(createdApp -> { + PageDTO pageDTO = new PageDTO(); + pageDTO.setApplicationId(application.getId()); + pageDTO.setName("Home Page"); + return applicationPageService.createPage(pageDTO).thenReturn(createdApp); + }) + .zipWith(applicationJson) + .flatMap(objects -> importService + .restoreSnapshot( + workspaceId, + objects.getT2(), + objects.getT1().getId(), + null) + .map(importableArtifact -> (Application) importableArtifact) + .zipWith(Mono.just(objects.getT1()))) + .flatMap(objects -> { + Application newApp = objects.getT1(); + Application oldApp = objects.getT2(); + // after import, application id should not change + assert Objects.equals(newApp.getId(), oldApp.getId()); + + Mono> pageList = newPageService + .findNewPagesByApplicationId(newApp.getId(), MANAGE_PAGES) + .collectList(); + Mono> actionList = newActionService + .findAllByApplicationIdAndViewMode(newApp.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + Mono> actionCollectionList = actionCollectionService + .findAllByApplicationIdAndViewMode(newApp.getId(), false, MANAGE_ACTIONS, null) + .collectList(); + return Mono.zip(Mono.just(newApp), pageList, actionList, actionCollectionList); + }); + + StepVerifier.create(importedApplication) + .assertNext(tuple -> { + List pageList = tuple.getT2(); + List actionList = tuple.getT3(); + List actionCollectionList = tuple.getT4(); + + assertThat(pageList.size()).isEqualTo(2); + assertThat(actionList.size()).isEqualTo(3); + + List pageNames = pageList.stream() + .map(p -> p.getUnpublishedPage().getName()) + .collect(Collectors.toList()); + + List actionNames = actionList.stream() + .map(p -> p.getUnpublishedAction().getName()) + .collect(Collectors.toList()); + + List actionCollectionNames = actionCollectionList.stream() + .map(p -> p.getUnpublishedCollection().getName()) + .collect(Collectors.toList()); + + // Verify the pages after importing the application + assertThat(pageNames).contains("Page1", "Page2"); + + // Verify the actions after importing the application + assertThat(actionNames).contains("api_wo_auth", "get_users", "run"); + + // Verify the actionCollections after importing the application + assertThat(actionCollectionNames).contains("JSObject1", "JSObject2"); + }) + .verifyComplete(); + } + + /** + * Testcase for updating the existing application: + * 1. Import application in org + * 2. Add new page to the imported application + * 3. User tries to import application from same application json file + * 4. Added page will be removed + *

+ * We don't have to test all the flows for other resources like actions, JSObjects, themes as these are already + * covered as a part of discard functionality + */ + @Test + @WithUserDetails(value = "api_user") + public void extractFileAndUpdateApplication_addNewPageAfterImport_addedPageRemoved() { + + /* + 1. Import application + 2. Add single page to imported app + 3. Import the application from same JSON with applicationId + 4. Added page should be deleted from DB + */ + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + String workspaceId = createTemplateWorkspace().getId(); + final Mono resultMonoWithoutDiscardOperation = importService + .extractArtifactExchangeJsonAndSaveArtifact(filePart, workspaceId, null, ArtifactJsonType.APPLICATION) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO) + .flatMap(applicationImportDTO -> { + PageDTO page = new PageDTO(); + page.setName("discard-page-test"); + page.setApplicationId(applicationImportDTO.getApplication().getId()); + return applicationPageService.createPage(page); + }) + .flatMap(page -> applicationRepository.findById(page.getApplicationId())) + .cache(); + + StepVerifier.create(resultMonoWithoutDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List pageList = tuple.getT2(); + + assertThat(application.getName()).isEqualTo("valid_application"); + assertThat(application.getWorkspaceId()).isNotNull(); + assertThat(application.getPages()).hasSize(3); + assertThat(application.getPublishedPages()).hasSize(1); + assertThat(application.getModifiedBy()).isEqualTo("api_user"); + assertThat(application.getUpdatedAt()).isNotNull(); + assertThat(application.getEditModeThemeId()).isNotNull(); + assertThat(application.getPublishedModeThemeId()).isNotNull(); + + assertThat(pageList).hasSize(3); + + ApplicationPage defaultAppPage = application.getPages().stream() + .filter(ApplicationPage::getIsDefault) + .findFirst() + .orElse(null); + assertThat(defaultAppPage).isNotNull(); + + PageDTO defaultPageDTO = pageList.stream() + .filter(pageDTO -> pageDTO.getId().equals(defaultAppPage.getId())) + .findFirst() + .orElse(null); + + assertThat(defaultPageDTO).isNotNull(); + assertThat(defaultPageDTO.getLayouts().get(0).getLayoutOnLoadActions()) + .isNotEmpty(); + + List pageNames = new ArrayList<>(); + pageList.forEach(page -> pageNames.add(page.getName())); + assertThat(pageNames).contains("discard-page-test"); + }) + .verifyComplete(); + + // Import the same application again to find if the added page is deleted + final Mono resultMonoWithDiscardOperation = resultMonoWithoutDiscardOperation + .flatMap(importedApplication -> applicationService.save(importedApplication)) + .flatMap(savedApplication -> importService.extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspaceId, savedApplication.getId(), ArtifactJsonType.APPLICATION)) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO) + .map(ApplicationImportDTO::getApplication); + + StepVerifier.create(resultMonoWithDiscardOperation.flatMap(application -> Mono.zip( + Mono.just(application), + newPageService + .findByApplicationId(application.getId(), MANAGE_PAGES, false) + .collectList()))) + .assertNext(tuple -> { + final Application application = tuple.getT1(); + final List pageList = tuple.getT2(); + + assertThat(application.getPages()).hasSize(2); + assertThat(application.getPublishedPages()).hasSize(1); + + assertThat(pageList).hasSize(2); + + List pageNames = new ArrayList<>(); + pageList.forEach(page -> pageNames.add(page.getName())); + assertThat(pageNames).doesNotContain("discard-page-test"); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void extractFileAndUpdateExistingApplication_gitConnectedApplication_throwUnsupportedOperationException() { + + /* + 1. Create application and mock git connectivity + 2. Import the application from valid JSON with saved applicationId + 3. Unsupported operation exception should be thrown + */ + + // Create application connected to git + Application testApplication = new Application(); + testApplication.setName( + "extractFileAndUpdateExistingApplication_gitConnectedApplication_throwUnsupportedOperationException"); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + GitApplicationMetadata gitData = new GitApplicationMetadata(); + gitData.setRemoteUrl("git@example.com:username/git-repo.git"); + testApplication.setGitApplicationMetadata(gitData); + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application1 -> { + application1.getGitApplicationMetadata().setDefaultApplicationId(application1.getId()); + return applicationService.save(application1); + }) + .block(); + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + final Mono resultMono = importService + .extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspaceId, application.getId(), ArtifactJsonType.APPLICATION) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO); + + StepVerifier.create(resultMono) + .expectErrorMatches(throwable -> throwable instanceof AppsmithException + && throwable + .getMessage() + .equals( + AppsmithError.UNSUPPORTED_IMPORT_OPERATION_FOR_GIT_CONNECTED_APPLICATION + .getMessage())) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void createExportAppJsonWithCustomJSLibTest() { + CustomJSLib jsLib = new CustomJSLib("TestLib", Set.of("accessor1"), "url", "docsUrl", "1.0", "defs_string"); + Mono addJSLibMonoCached = customJSLibService + .addJSLibsToContext(testAppId, CreatorContextType.APPLICATION, Set.of(jsLib), null, false) + .flatMap(isJSLibAdded -> + Mono.zip(Mono.just(isJSLibAdded), applicationPageService.publish(testAppId, true))) + .map(tuple2 -> { + Boolean isJSLibAdded = tuple2.getT1(); + Application application = tuple2.getT2(); + return isJSLibAdded; + }) + .cache(); + Mono getExportedAppMono = + addJSLibMonoCached.then(exportApplicationService.exportApplicationById(testAppId, "")); + StepVerifier.create(Mono.zip(addJSLibMonoCached, getExportedAppMono)) + .assertNext(tuple2 -> { + Boolean isJSLibAdded = tuple2.getT1(); + assertEquals(true, isJSLibAdded); + ApplicationJson exportedAppJson = tuple2.getT2(); + assertEquals(1, exportedAppJson.getCustomJSLibList().size()); + CustomJSLib exportedJSLib = + exportedAppJson.getCustomJSLibList().get(0); + assertEquals(jsLib.getName(), exportedJSLib.getName()); + assertEquals(jsLib.getAccessor(), exportedJSLib.getAccessor()); + assertEquals(jsLib.getUrl(), exportedJSLib.getUrl()); + assertEquals(jsLib.getDocsUrl(), exportedJSLib.getDocsUrl()); + assertEquals(jsLib.getVersion(), exportedJSLib.getVersion()); + assertEquals(jsLib.getDefs(), exportedJSLib.getDefs()); + assertEquals( + getDTOFromCustomJSLib(jsLib), + exportedAppJson + .getExportedApplication() + .getUnpublishedCustomJSLibs() + .toArray()[0]); + assertEquals( + 1, + exportedAppJson + .getExportedApplication() + .getUnpublishedCustomJSLibs() + .size()); + assertEquals( + 0, + exportedAppJson + .getExportedApplication() + .getPublishedCustomJSLibs() + .size()); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void extractFileAndUpdateExistingApplication_existingApplication_applicationNameAndSlugRemainsUnchanged() { + + /* + 1. Create application + 2. Import the application from valid JSON with saved applicationId + 3. Name and slug will not be updated by the incoming changes from the json file + */ + + Application testApplication = new Application(); + final String appName = UUID.randomUUID().toString(); + testApplication.setName(appName); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + Application application = applicationPageService + .createApplication(testApplication, workspaceId) + .block(); + + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + final Mono resultMono = importService + .extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspaceId, application.getId(), ArtifactJsonType.APPLICATION) + .map(importableArtifactDTO -> (ApplicationImportDTO) importableArtifactDTO); + + StepVerifier.create(resultMono) + .assertNext(applicationImportDTO -> { + Application application1 = applicationImportDTO.getApplication(); + assertThat(application1.getName()).isEqualTo(appName); + assertThat(application1.getSlug()).isEqualTo(appName); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails(value = "api_user") + public void mergeApplicationJsonWithApplication_WhenNoPermissionToCreatePage_Fails() { + Application testApplication = new Application(); + final String appName = UUID.randomUUID().toString(); + testApplication.setName(appName); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + + Mono applicationImportDTOMono = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application -> { + // remove page create permission from this application for current user + application.getPolicies().removeIf(policy -> policy.getPermission() + .equals(applicationPermission + .getPageCreatePermission() + .getValue())); + return applicationRepository.save(application); + }) + .flatMap(application -> { + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + return importService + .extractArtifactExchangeJson(filePart, ArtifactJsonType.APPLICATION) + .map(artifactExchangeJson -> (ApplicationJson) artifactExchangeJson) + .flatMap(applicationJson -> importService.mergeArtifactExchangeJsonWithImportableArtifact( + workspaceId, application.getId(), null, applicationJson, null)) + .map(importableArtifact -> (Application) importableArtifact); + }); + + StepVerifier.create(applicationImportDTOMono) + .expectError(AppsmithException.class) + .verify(); + } + + @Test + @WithUserDetails(value = "api_user") + public void extractFileAndUpdateNonGitConnectedApplication_WhenNoPermissionToCreatePage_Fails() { + Application testApplication = new Application(); + final String appName = UUID.randomUUID().toString(); + testApplication.setName(appName); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + + Mono applicationImportDTOMono = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application -> { + // remove page create permission from this application for current user + application.getPolicies().removeIf(policy -> policy.getPermission() + .equals(applicationPermission + .getPageCreatePermission() + .getValue())); + return applicationRepository.save(application); + }) + .flatMap(application -> { + FilePart filePart = createFilePart("test_assets/ImportExportServiceTest/valid-application.json"); + return importService + .extractArtifactExchangeJsonAndSaveArtifact( + filePart, workspaceId, application.getId(), ArtifactJsonType.APPLICATION) + .map(artifactImportDTO -> (ApplicationImportDTO) artifactImportDTO); + }); + + StepVerifier.create(applicationImportDTOMono) + .expectError(AppsmithException.class) + .verify(); + } + + private Mono createActionToPage(String actionName, String pageId) { + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(datasourceMap.get("DS1")); + action.setName(actionName); + action.setPageId(pageId); + return layoutActionService.createAction(action); + } + + private Mono createActionCollectionToPage(Application application, int pageIndex) { + ActionCollectionDTO actionCollectionDTO1 = new ActionCollectionDTO(); + actionCollectionDTO1.setName("TestJsObject"); + actionCollectionDTO1.setPageId(application.getPages().get(pageIndex).getId()); + actionCollectionDTO1.setApplicationId(application.getId()); + actionCollectionDTO1.setWorkspaceId(application.getWorkspaceId()); + actionCollectionDTO1.setPluginId(jsDatasource.getPluginId()); + ActionDTO action1 = new ActionDTO(); + action1.setName("testMethod"); + action1.setActionConfiguration(new ActionConfiguration()); + action1.getActionConfiguration().setBody("mockBody"); + actionCollectionDTO1.setActions(List.of(action1)); + actionCollectionDTO1.setPluginType(PluginType.JS); + return layoutCollectionService.createCollection(actionCollectionDTO1, null); + } + + @Test + @WithUserDetails("api_user") + public void exportApplicationByWhen_WhenGitConnectedAndPageRenamed_QueriesAreInUpdatedResources() { + String renamedPageName = "Renamed Page"; + // create an application + Application testApplication = new Application(); + final String appName = UUID.randomUUID().toString(); + testApplication.setName(appName); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setClientSchemaVersion(JsonSchemaVersions.clientVersion); + testApplication.setServerSchemaVersion(JsonSchemaVersions.serverVersion); + + Mono applicationJsonMono = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application -> { + // add another page to the application + PageDTO pageDTO = new PageDTO(); + pageDTO.setName("second_page"); + pageDTO.setApplicationId(application.getId()); + return applicationPageService + .createPage(pageDTO) // get the updated application + .then(applicationService.findById(application.getId())); + }) + .flatMap(application -> { + assert application.getPages().size() == 2; + // add one action to each of the pages + return createActionToPage( + "first_page_action", + application.getPages().get(0).getId()) + .then(createActionToPage( + "second_page_action", + application.getPages().get(1).getId())) + .thenReturn(application); + }) + .flatMap(application -> { + // add one action collection to each of the pages + return createActionCollectionToPage(application, 0) + .then(createActionCollectionToPage(application, 1)) + .thenReturn(application); + }) + .flatMap(application -> { + // set git meta data for the application and set a last commit date + GitApplicationMetadata gitApplicationMetadata = new GitApplicationMetadata(); + // add buffer of 5 seconds so that the last commit date is definitely after the last updated date + gitApplicationMetadata.setLastCommittedAt(Instant.now()); + application.setGitApplicationMetadata(gitApplicationMetadata); + return applicationRepository.save(application); + }) + .delayElement(Duration.ofMillis( + 100)) // to make sure the last commit date is definitely after the last updated date + .flatMap(application -> { + // rename the page + ApplicationPage applicationPage = application.getPages().get(0); + PageDTO pageDTO = new PageDTO(); + pageDTO.setName(renamedPageName); + return newPageService + .updatePage(applicationPage.getId(), pageDTO) + // export the application + .then(exportApplicationService.exportApplicationById( + application.getId(), SerialiseApplicationObjective.VERSION_CONTROL)); + }); + + // verify that the exported json has the updated page name, and the queries are in the updated resources + StepVerifier.create(applicationJsonMono) + .assertNext(applicationJson -> { + Map> updatedResources = applicationJson.getUpdatedResources(); + assertThat(updatedResources).isNotNull(); + Set updatedPageNames = updatedResources.get(FieldName.PAGE_LIST); + Set updatedActionNames = updatedResources.get(FieldName.ACTION_LIST); + Set updatedActionCollectionNames = updatedResources.get(FieldName.ACTION_COLLECTION_LIST); + + assertThat(updatedPageNames).isNotNull(); + assertThat(updatedActionNames).isNotNull(); + assertThat(updatedActionCollectionNames).isNotNull(); + + // only the first page should be present in the updated resources + assertThat(updatedPageNames.size()).isEqualTo(1); + assertThat(updatedPageNames).contains(renamedPageName); + + // only actions from first page should be present in the updated resources + // 1 query + 1 method from action collection + assertThat(updatedActionNames.size()).isEqualTo(2); + assertThat(updatedActionNames).contains("first_page_action" + NAME_SEPARATOR + renamedPageName); + assertThat(updatedActionNames) + .contains("TestJsObject.testMethod" + NAME_SEPARATOR + renamedPageName); + + // only action collections from first page should be present in the updated resources + assertThat(updatedActionCollectionNames.size()).isEqualTo(1); + assertThat(updatedActionCollectionNames) + .contains("TestJsObject" + NAME_SEPARATOR + renamedPageName); + }) + .verifyComplete(); + } + + @Test + @WithUserDetails("api_user") + public void exportApplicationByWhen_WhenGitConnectedAndDatasourceRenamed_QueriesAreInUpdatedResources() { + // create an application + Application testApplication = new Application(); + final String appName = UUID.randomUUID().toString(); + testApplication.setName(appName); + testApplication.setWorkspaceId(workspaceId); + testApplication.setUpdatedAt(Instant.now()); + testApplication.setLastDeployedAt(Instant.now()); + testApplication.setClientSchemaVersion(JsonSchemaVersions.clientVersion); + testApplication.setServerSchemaVersion(JsonSchemaVersions.serverVersion); + + Mono applicationJsonMono = applicationPageService + .createApplication(testApplication, workspaceId) + .flatMap(application -> { + // add a datasource to the workspace + Datasource ds1 = new Datasource(); + ds1.setName("DS_FOR_RENAME_TEST"); + ds1.setWorkspaceId(workspaceId); + ds1.setPluginId(installedPlugin.getId()); + final DatasourceConfiguration datasourceConfiguration = new DatasourceConfiguration(); + datasourceConfiguration.setUrl("http://example.org/get"); + datasourceConfiguration.setHeaders(List.of(new Property("X-Answer", "42"))); + + HashMap storages1 = new HashMap<>(); + storages1.put( + defaultEnvironmentId, + new DatasourceStorageDTO(null, defaultEnvironmentId, datasourceConfiguration)); + ds1.setDatasourceStorages(storages1); + return datasourceService.create(ds1).zipWith(Mono.just(application)); + }) + .flatMap(objects -> { + Datasource datasource = objects.getT1(); + Application application = objects.getT2(); + + // create an action with the datasource + ActionDTO action = new ActionDTO(); + ActionConfiguration actionConfiguration = new ActionConfiguration(); + actionConfiguration.setHttpMethod(HttpMethod.GET); + action.setActionConfiguration(actionConfiguration); + action.setDatasource(datasource); + action.setName("MyAction"); + action.setPageId(application.getPages().get(0).getId()); + return layoutActionService.createAction(action).thenReturn(objects); + }) + .flatMap(objects -> { + Application application = objects.getT2(); + // set git meta data for the application and set a last commit date + GitApplicationMetadata gitApplicationMetadata = new GitApplicationMetadata(); + // add buffer of 5 seconds so that the last commit date is definitely after the last updated date + gitApplicationMetadata.setLastCommittedAt(Instant.now()); + application.setGitApplicationMetadata(gitApplicationMetadata); + return applicationRepository.save(application).thenReturn(objects); + }) + .delayElement(Duration.ofMillis( + 100)) // to make sure the last commit date is definitely after the last updated date + .flatMap(objects -> { + // rename the datasource + Datasource datasource = objects.getT1(); + Application application = objects.getT2(); + datasource.setName("DS_FOR_RENAME_TEST_RENAMED"); + return datasourceService + .save(datasource) + .then(exportApplicationService.exportApplicationById( + application.getId(), SerialiseApplicationObjective.VERSION_CONTROL)); + }); + + // verify that the exported json has the updated page name, and the queries are in the updated resources + StepVerifier.create(applicationJsonMono) + .assertNext(applicationJson -> { + Map> updatedResources = applicationJson.getUpdatedResources(); + assertThat(updatedResources).isNotNull(); + Set updatedActionNames = updatedResources.get(FieldName.ACTION_LIST); + assertThat(updatedActionNames).isNotNull(); + + // action should be present in the updated resources although action not updated but datasource is + assertThat(updatedActionNames.size()).isEqualTo(1); + updatedActionNames.forEach(actionName -> { + assertThat(actionName).contains("MyAction"); + }); + }) + .verifyComplete(); + } +} diff --git a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PartialExportServiceTest.java b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PartialExportServiceTest.java index 8bebe5e3c2..ed40c09bc0 100644 --- a/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PartialExportServiceTest.java +++ b/app/server/appsmith-server/src/test/java/com/appsmith/server/services/PartialExportServiceTest.java @@ -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 partialExportFileDTOMono = partialExportService.getPartialExportResources( + application.getId(), savedPage.getId(), "master", partialExportFileDTO); + + StepVerifier.create(partialExportFileDTOMono) + .assertNext(applicationJson -> { + assertThat(applicationJson.getDatasourceList().size()).isEqualTo(2); + List 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(); + } }