Feature/multi page applications (Task ID - CU-2h1bfvw) (#4729)

* Add routes for multi-page apps

* Modify Editor, Viewer and Inspector to accept new app structure

* Show a page selector on left side bar

* Align component deletion logic with new app schema

* Make subcontainer work with multi-page apps

* Load components state properly in viewer

* Use UUID instead of handle for pages

* Display sidebar on viewer to switch pages

* Add proper URL suffixing for pages in viewer

* Add action to switch page

* Revert translation file back to its pre-existing linting

* Fix bug that caused modal to not open/close

* Add support for query params in page switch

* Fix the issue that caused navigation to fail while accessed via slug

* Add missing SwitchPage file

* Add support for page level variables

* Add migration to convert existing apps to new schema

* Add rollback for converting multi-page definitions back to single-page

* Fix migration for multi-page apps

* Adapt import/export service for multi-pages

* [improvements] Multi-page applications (#4755)

* UI updates for page selector popup card

* delete page

* delete page check: if only one page exits

* switch to home page if the selected page is removed

* adds and switch to new page

* updating page name

* updates to home page and starting page

* handle updating the home page when home page is deleted

* search box for filtering pages and minor style updates for the page handler card

* header search box style fixes

* for creating a new page, page handle needs to be unique

* seperating into smaller components

* updated pinned icon for page selector styles and settinf styles

* Leftsidebar header ui component

* handle dark theme

* page handle ui and dark theme fixes for page menu

* page handler edit modal

* pinned state and update pinned state for menu options triggered

* dark theme fixes for edit modal

* handle on update should not be empty or prev

* page handler updater

* added loading state for saving

* handles cancels

* fixes slug ui

* fixes crash for older app versions

* updates the query params when handle gets an update

* update homePage to homePageId

* removes console.log

* go back to the popover for modal close

* fixes: Difficult to select page

* fixes: Difficult to select the three-dot menu

* fixes: on visiting the root url, navigate to homepage on viewer

* adds tooltip for url

* updates the page selector sidebar with sync with query manager

* refactor and cleanup

* refactor and cleanup

* Compute component state when page is switched

* modal should not close on click outside

* disable save button if there is not change in the page handle input

* should show/hide page menu when hovered

* page icon

* updates delete icon for disabled state

* query manager should always be on top of page selector

* checks if homePage key exists in pages def

* updates page handler menu

* updates the clear icon

* page handler menu position

* page handler menu position

* handle icon

* alert msg

* global settings handler for updating viewer page navigation

* show/hode page navigation for viewer

* info text for toggle

* Multipages:with sortable list [DnD] (#4783)

* applied sortable list

* on sort updates the definitions

* fixies: app crash for dnd

* viwer: canvas width should be 100% when navigation drawer is disbaled

* fixes: homepage/startpage  reload

* clean up

Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com>

* Multipage UI viewer (#4801)

* new ui changes for viewer pages

* fixes postions for debugger and datasources popover

* removes console.log

* Multipage : hide page and unhide page feature (#4803)

* adds: ability to hide pages

* hides pages in viewer

* unhide page

* hide icon

* allow accessing hidden pages from url

* add: duplicate page (#4802)

* add: duplicate page

* do not copy the  same references from the original page

* page name and page handler should be unique for duplicate pages too

* Add support for on-page-load events

* Add icon from page settings menu item

* Convert existing templates to multi-page schema

* error logs for page level and app level errors (#4842)

* Adapt comments feature for multi-pages

* [Bugfix] multipage - page menu interactions (#4844)

* fixes: menu popup interaction

* fixes: on modal input focus, we switch the page

* Adapt multi-player to multi-pages

* Add editingPageId to ymap

* Log self, others and editor props in real-time avatar generation

* Save editing page id to appDef

* Add editingPageId to presence in RealtimeCursors

* adds no results ui for empty search results (#4869)

* page icon updated (#4870)

* fixes:Version switching crashes if the target version does not contain the current page (#4868)

* Remove unnecessary setting of editingPageId on ymap

* Remove unnecessary console.log

* [Bugfix] Multipages: widget inspector event popover unmounts (#4887)

* introduced a local state for events

* cleaned up inspector.jsx

* fixes: table widget inspector event accordion

* Do not run switchPage twice when viewer is loaded

* Preview should open the currently editing page

* Properly place navigation and canvas in viewer

* Update app definition whenever event manager changes are made

* Add support for browser back and forward button in multi-pages

* Rename handleBackButton to handlePageSwitchingBasedOnURLparam

* Add support for cut/copy/paste and clone

* Fix the crash caused by boxShadow

* Add support for background colors in viewer in multi-pages

* Run queries to be run on load on viewer, in multi-pages

* Fix issue that caused inspector popovers to collapse

* resolves workspace vars in viewer mode (#4892)

* Multipage : Navigation for Mobile-ui (#4814)

* refactored to components

* burger menu for mobile ui

* merge conflict fix for hidden pages

* hamburger menu positioned in the header

* viewer header reafctored

* viewer mobile page manu styles

* handles dark theme

* mobile menu with dark mode toggle in the footer

* components are moved to page level, handle for mobile layout

* style fixes

* removing unwanted code block

* dark theme fixes

* style fixes

* fixes: events are sortable (#4895)

* fixes: events are sortable

* Remove uneccesarily repeated call of setEvents in EventManager

Co-authored-by: Sherfin Shamsudeen <sherfin94@gmail.com>

* renamed settings to Event handlers (#4898)

* updates the page setting title to Page Events

* temp commit

* Add support for setting max width in percentage

* fixes: paramUpdates for boxes: 🙌🏻

* [Bugfix] Multipage - viewer canvas dark theme (#4897)

* fixes: darktheme bg for viewer canvas

* reverts canvas size

* Fix for inspector bouncing back to previous values

* resolves pages variables in pythong and js transformation (#4905)

* csa support to event manager for pages (#4907)

* Add support for setting canvas width in percentages

* Persist page level variables across page switches

* latest definitions is merged with the current appdef (#4914)

* latest definitions is merged with the current appdef

* mutating the local obj

* cleanup

* iterate through pages for new versions are created

Co-authored-by: Arpit <arpitnath42@gmail.com>
This commit is contained in:
Sherfin Shamsudeen 2022-12-08 17:51:09 +05:30 committed by GitHub
parent 62724dec15
commit 642c5caa71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
92 changed files with 11051 additions and 28436 deletions

View file

@ -0,0 +1,8 @@
<svg width="4" height="16" viewBox="0 0 4 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0.333252 2.16667C0.333252 1.24619 1.07944 0.5 1.99992 0.5C2.92039 0.5 3.66659 1.24619 3.66659 2.16667C3.66659 3.08714 2.92039 3.83333 1.99992 3.83333C1.07944 3.83333 0.333252 3.08714 0.333252 2.16667ZM0.333252 8C0.333252 7.07953 1.07944 6.33333 1.99992 6.33333C2.92039 6.33333 3.66659 7.07953 3.66659 8C3.66659 8.92047 2.92039 9.66667 1.99992 9.66667C1.07944 9.66667 0.333252 8.92047 0.333252 8ZM0.333252 13.8333C0.333252 12.9129 1.07944 12.1667 1.99992 12.1667C2.92039 12.1667 3.66659 12.9129 3.66659 13.8333C3.66659 14.7538 2.92039 15.5 1.99992 15.5C1.07944 15.5 0.333252 14.7538 0.333252 13.8333Z"
fill="#7E868C"
/>
</svg>

After

Width:  |  Height:  |  Size: 807 B

View file

@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.99984 2.33317C5.31794 2.33317 2.33317 5.31794 2.33317 8.99984C2.33317 12.6817 5.31794 15.6665 8.99984 15.6665C12.6817 15.6665 15.6665 12.6817 15.6665 8.99984C15.6665 5.31794 12.6817 2.33317 8.99984 2.33317ZM0.666504 8.99984C0.666504 4.39746 4.39746 0.666504 8.99984 0.666504C13.6022 0.666504 17.3332 4.39746 17.3332 8.99984C17.3332 13.6022 13.6022 17.3332 8.99984 17.3332C4.39746 17.3332 0.666504 13.6022 0.666504 8.99984ZM8.1665 5.6665C8.1665 5.20627 8.5396 4.83317 8.99984 4.83317H9.00817C9.46841 4.83317 9.8415 5.20627 9.8415 5.6665C9.8415 6.12674 9.46841 6.49984 9.00817 6.49984H8.99984C8.5396 6.49984 8.1665 6.12674 8.1665 5.6665ZM7.33317 8.99984C7.33317 8.5396 7.70627 8.1665 8.1665 8.1665H8.99984C9.46008 8.1665 9.83317 8.5396 9.83317 8.99984V11.4998C10.2934 11.4998 10.6665 11.8729 10.6665 12.3332C10.6665 12.7934 10.2934 13.1665 9.83317 13.1665H8.99984C8.5396 13.1665 8.1665 12.7934 8.1665 12.3332V9.83317C7.70627 9.83317 7.33317 9.46008 7.33317 8.99984Z" fill="#F76808"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.72386 0.72402C3.97391 0.473972 4.31304 0.333496 4.66667 0.333496H7.33333C7.68696 0.333496 8.02609 0.473972 8.27614 0.72402C8.52619 0.974069 8.66667 1.31321 8.66667 1.66683V3.00016H10.6589C10.6636 3.00011 10.6683 3.00011 10.673 3.00016H11.3333C11.7015 3.00016 12 3.29864 12 3.66683C12 4.03502 11.7015 4.3335 11.3333 4.3335H11.2801L10.6664 11.6974C10.6585 12.2167 10.4488 12.7132 10.0809 13.081C9.70581 13.4561 9.1971 13.6668 8.66667 13.6668H3.33333C2.8029 13.6668 2.29419 13.4561 1.91912 13.081C1.55125 12.7132 1.34148 12.2167 1.33357 11.6974L0.719911 4.3335H0.666667C0.298477 4.3335 0 4.03502 0 3.66683C0 3.29864 0.298477 3.00016 0.666667 3.00016H1.32702C1.33174 3.00011 1.33644 3.00011 1.34113 3.00016H3.33333V1.66683C3.33333 1.31321 3.47381 0.974069 3.72386 0.72402ZM2.05787 4.3335L2.66436 11.6115C2.6659 11.6299 2.66667 11.6484 2.66667 11.6668C2.66667 11.8436 2.7369 12.0132 2.86193 12.1382C2.98695 12.2633 3.15652 12.3335 3.33333 12.3335H8.66667C8.84348 12.3335 9.01305 12.2633 9.13807 12.1382C9.2631 12.0132 9.33333 11.8436 9.33333 11.6668C9.33333 11.6484 9.3341 11.6299 9.33564 11.6115L9.94213 4.3335H2.05787ZM7.33333 3.00016H4.66667V1.66683H7.33333V3.00016ZM4.19526 7.47157C3.93491 7.21122 3.93491 6.78911 4.19526 6.52876C4.45561 6.26841 4.87772 6.26841 5.13807 6.52876L6 7.39069L6.86193 6.52876C7.12228 6.26841 7.54439 6.26841 7.80474 6.52876C8.06509 6.78911 8.06509 7.21122 7.80474 7.47157L6.94281 8.3335L7.80474 9.19543C8.06509 9.45577 8.06509 9.87788 7.80474 10.1382C7.54439 10.3986 7.12228 10.3986 6.86193 10.1382L6 9.27631L5.13807 10.1382C4.87772 10.3986 4.45561 10.3986 4.19526 10.1382C3.93491 9.87788 3.93491 9.45577 4.19526 9.19543L5.05719 8.3335L4.19526 7.47157Z" fill="#C1C8CD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0.585786 0.585786C0.960859 0.210714 1.46957 0 2 0H7.33333C7.86377 0 8.37247 0.210714 8.74755 0.585786C9.12262 0.960859 9.33333 1.46957 9.33333 2V2.66667H10C11.1046 2.66667 12 3.5621 12 4.66667V10C12 11.1046 11.1046 12 10 12H4.66667C3.5621 12 2.66667 11.1046 2.66667 10V9.33333H2C1.46957 9.33333 0.960859 9.12262 0.585786 8.74755C0.210714 8.37247 0 7.86377 0 7.33333V2C0 1.46957 0.210714 0.960859 0.585786 0.585786ZM4 10C4 10.3682 4.29848 10.6667 4.66667 10.6667H10C10.3682 10.6667 10.6667 10.3682 10.6667 10V4.66667C10.6667 4.29848 10.3682 4 10 4H4.66667C4.29848 4 4 4.29848 4 4.66667V10ZM8 2.66667H4.66667C3.5621 2.66667 2.66667 3.5621 2.66667 4.66667V8H2C1.82319 8 1.65362 7.92976 1.5286 7.80474C1.40357 7.67971 1.33333 7.51014 1.33333 7.33333V2C1.33333 1.82319 1.40357 1.65362 1.5286 1.5286C1.65362 1.40357 1.82319 1.33333 2 1.33333H7.33333C7.51014 1.33333 7.67971 1.40357 7.80474 1.5286C7.92976 1.65362 8 1.82319 8 2V2.66667Z" fill="#C1C8CD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13.5371 20.7996H10.4389C10.1825 20.7996 9.95825 20.7179 9.76623 20.5543C9.57364 20.3902 9.45598 20.1729 9.41324 19.9022L9.15684 18.0433C9.01439 17.9863 8.85058 17.9045 8.66539 17.798C8.48021 17.6909 8.3164 17.5803 8.17395 17.4664L6.44321 18.2142C6.20105 18.3282 5.95548 18.3387 5.70648 18.2458C5.45691 18.1535 5.2609 17.9934 5.11845 17.7655L3.55866 14.9664C3.43045 14.7385 3.39484 14.4997 3.45182 14.2502C3.5088 14.0012 3.637 13.7983 3.83643 13.6416L5.28939 12.5306C5.27515 12.4308 5.26461 12.3311 5.25777 12.2314C5.25036 12.1317 5.24666 12.032 5.24666 11.9323C5.24666 11.8468 5.25036 11.7613 5.25777 11.6759C5.26461 11.5904 5.27515 11.4907 5.28939 11.3767L3.83643 10.3084C3.62276 10.1659 3.49455 9.96279 3.45182 9.69898C3.40909 9.43574 3.45182 9.19016 3.58002 8.96224L5.11845 6.24862C5.2609 6.03495 5.45321 5.87455 5.69537 5.76743C5.93753 5.66088 6.17256 5.66459 6.40048 5.77854L8.08848 6.48366C8.24517 6.3697 8.42323 6.25916 8.62266 6.15204C8.82209 6.04549 9.00727 5.94948 9.1782 5.86401L9.41324 4.04781C9.44173 3.77716 9.55569 3.55978 9.75512 3.39569C9.95454 3.23216 10.1825 3.15039 10.4389 3.15039H13.5371C13.8077 3.15039 14.0428 3.23216 14.2422 3.39569C14.4416 3.55978 14.5556 3.77716 14.5841 4.04781L14.8191 5.88538C15.0043 5.98509 15.1861 6.0811 15.3644 6.17341C15.5422 6.26628 15.7165 6.37682 15.8875 6.50503L17.5755 5.79991C17.8034 5.68595 18.0421 5.67513 18.2917 5.76743C18.5407 5.86031 18.7364 6.02071 18.8789 6.24862L20.4173 8.96224C20.5455 9.19016 20.5848 9.43574 20.5353 9.69898C20.4851 9.96279 20.3603 10.1659 20.1609 10.3084L18.6438 11.4408C18.6581 11.5405 18.6652 11.6368 18.6652 11.7297V11.9964C18.6652 12.0676 18.6618 12.1531 18.6549 12.2528C18.6475 12.3525 18.6438 12.4593 18.6438 12.5733L20.0968 13.663C20.3105 13.8055 20.4458 14.0049 20.5028 14.2613C20.5598 14.5177 20.517 14.7599 20.3746 14.9878L18.8148 17.7655C18.6866 17.9934 18.4977 18.1501 18.2481 18.2356C17.9991 18.321 17.7535 18.3068 17.5114 18.1928L15.7806 17.4664C15.6239 17.5803 15.4673 17.6837 15.3106 17.7766C15.1539 17.8689 14.9972 17.9507 14.8405 18.0219L14.5841 19.9022C14.5413 20.1729 14.424 20.3902 14.232 20.5543C14.0394 20.7179 13.8077 20.7996 13.5371 20.7996V20.7996ZM11.9346 15.116C12.8177 15.116 13.5656 14.8097 14.1781 14.1972C14.7906 13.5847 15.0969 12.8368 15.0969 11.9536C15.0969 11.0705 14.7906 10.3226 14.1781 9.71009C13.5656 9.09757 12.8177 8.79131 11.9346 8.79131C11.0656 8.79131 10.3212 9.09757 9.70127 9.71009C9.08191 10.3226 8.77223 11.0705 8.77223 11.9536C8.77223 12.8368 9.08191 13.5847 9.70127 14.1972C10.3212 14.8097 11.0656 15.116 11.9346 15.116V15.116ZM11.9346 13.2784C11.5642 13.2784 11.2508 13.1502 10.9944 12.8938C10.738 12.6374 10.6098 12.324 10.6098 11.9536C10.6098 11.5833 10.7417 11.2699 11.0055 11.0135C11.2688 10.7571 11.5856 10.6289 11.9559 10.6289C12.312 10.6289 12.6183 10.7571 12.8747 11.0135C13.1311 11.2699 13.2593 11.5833 13.2593 11.9536C13.2593 12.324 13.1311 12.6374 12.8747 12.8938C12.6183 13.1502 12.3049 13.2784 11.9346 13.2784ZM11.1226 18.9621H12.8106L13.1311 16.6544C13.587 16.5404 14.0038 16.3732 14.3815 16.1527C14.7587 15.9316 15.1254 15.6501 15.4815 15.3083L17.5541 16.1843L18.4302 14.7314L16.614 13.3425C16.6852 13.1146 16.735 12.8867 16.7635 12.6588C16.792 12.4308 16.8063 12.1958 16.8063 11.9536C16.8063 11.7115 16.7954 11.487 16.7738 11.2801C16.7527 11.0739 16.7065 10.8426 16.6353 10.5861L18.4302 9.21865L17.6182 7.76569L15.4815 8.62037C15.1966 8.30699 14.8442 8.02551 14.4243 7.77594C14.0038 7.52694 13.5727 7.35259 13.1311 7.25288L12.8747 4.98796H11.1226L10.8662 7.23151C10.3819 7.34547 9.9403 7.51982 9.54145 7.75458C9.14259 7.9899 8.77223 8.2785 8.43036 8.62037L6.37911 7.76569L5.52443 9.21865L7.2979 10.5861C7.22668 10.8568 7.17682 11.099 7.14833 11.3126C7.11984 11.5263 7.1056 11.74 7.1056 11.9536C7.1056 12.1388 7.11984 12.3454 7.14833 12.5733C7.17682 12.8012 7.22668 13.0647 7.2979 13.3639L5.52443 14.7314L6.37911 16.1843L8.45172 15.3083C8.83633 15.6644 9.22464 15.9527 9.61666 16.1732C10.0081 16.3943 10.4246 16.5618 10.8662 16.6758L11.1226 18.9621Z" fill="#C1C8CD"/>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 4C6.73478 4 6.48043 4.10536 6.29289 4.29289C6.10536 4.48043 6 4.73478 6 5V19C6 19.2652 6.10536 19.5196 6.29289 19.7071C6.48043 19.8946 6.73478 20 7 20H17C17.2652 20 17.5196 19.8946 17.7071 19.7071C17.8946 19.5196 18 19.2652 18 19V9H15C14.4696 9 13.9609 8.78929 13.5858 8.41421C13.2107 8.03914 13 7.53043 13 7V4H7ZM15 5.41421L16.5858 7H15V5.41421ZM4.87868 2.87868C5.44129 2.31607 6.20435 2 7 2H14C14.2652 2 14.5196 2.10536 14.7071 2.29289L19.7071 7.29289C19.8946 7.48043 20 7.73478 20 8V19C20 19.7957 19.6839 20.5587 19.1213 21.1213C18.5587 21.6839 17.7957 22 17 22H7C6.20435 22 5.44129 21.6839 4.87868 21.1213C4.31607 20.5587 4 19.7957 4 19V5C4 4.20435 4.31607 3.44129 4.87868 2.87868ZM8 13C8 12.4477 8.44772 12 9 12H15C15.5523 12 16 12.4477 16 13C16 13.5523 15.5523 14 15 14H9C8.44772 14 8 13.5523 8 13ZM8 17C8 16.4477 8.44772 16 9 16H15C15.5523 16 16 16.4477 16 17C16 17.5523 15.5523 18 15 18H9C8.44772 18 8 17.5523 8 17Z" fill="#2C3E50"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-eye-off" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#C1C8CD" fill="none" stroke-linecap="round" stroke-linejoin="round">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<line x1="3" y1="3" x2="21" y2="21" />
<path d="M10.584 10.587a2 2 0 0 0 2.828 2.83" />
<path d="M9.363 5.365a9.466 9.466 0 0 1 2.637 -.365c4 0 7.333 2.333 10 7c-.778 1.361 -1.612 2.524 -2.503 3.488m-2.14 1.861c-1.631 1.1 -3.415 1.651 -5.357 1.651c-4 0 -7.333 -2.333 -10 -7c1.369 -2.395 2.913 -4.175 4.632 -5.341" />
</svg>

After

Width:  |  Height:  |  Size: 607 B

View file

@ -0,0 +1,3 @@
<svg width="16" height="12" viewBox="0 0 16 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.10641 5.99984C3.7717 8.75352 5.74169 9.99984 7.99984 9.99984C10.258 9.99984 12.228 8.75352 13.8933 5.99984C12.228 3.24616 10.258 1.99984 7.99984 1.99984C5.74169 1.99984 3.7717 3.24616 2.10641 5.99984ZM0.754349 5.66906C2.60464 2.43123 5.01316 0.666504 7.99984 0.666504C10.9865 0.666504 13.395 2.43123 15.2453 5.66906C15.3625 5.87403 15.3625 6.12565 15.2453 6.33061C13.395 9.56844 10.9865 11.3332 7.99984 11.3332C5.01316 11.3332 2.60464 9.56844 0.754349 6.33061C0.637222 6.12565 0.637222 5.87403 0.754349 5.66906ZM7.99984 5.33317C7.63165 5.33317 7.33317 5.63165 7.33317 5.99984C7.33317 6.36803 7.63165 6.6665 7.99984 6.6665C8.36803 6.6665 8.6665 6.36803 8.6665 5.99984C8.6665 5.63165 8.36803 5.33317 7.99984 5.33317ZM5.99984 5.99984C5.99984 4.89527 6.89527 3.99984 7.99984 3.99984C9.10441 3.99984 9.99984 4.89527 9.99984 5.99984C9.99984 7.10441 9.10441 7.99984 7.99984 7.99984C6.89527 7.99984 5.99984 7.10441 5.99984 5.99984Z" fill="#C1C8CD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.52878 0.528758C6.78913 0.268409 7.21124 0.268409 7.47159 0.528758L13.4716 6.52876C13.6623 6.71942 13.7193 7.00617 13.6161 7.25529C13.5129 7.5044 13.2698 7.66683 13.0002 7.66683H12.3335V11.6668C12.3335 12.1973 12.1228 12.706 11.7477 13.081C11.3727 13.4561 10.8639 13.6668 10.3335 13.6668H3.66685C3.13642 13.6668 2.62771 13.4561 2.25264 13.081C1.87756 12.706 1.66685 12.1973 1.66685 11.6668V7.66683H1.00018C0.730541 7.66683 0.48745 7.5044 0.384263 7.25529C0.281075 7.00617 0.338112 6.71942 0.528778 6.52876L6.52878 0.528758ZM2.56746 6.3757C2.82025 6.47044 3.00018 6.71429 3.00018 7.00016V11.6668C3.00018 11.8436 3.07042 12.0132 3.19544 12.1382C3.32047 12.2633 3.49004 12.3335 3.66685 12.3335H10.3335C10.5103 12.3335 10.6799 12.2633 10.8049 12.1382C10.9299 12.0132 11.0002 11.8436 11.0002 11.6668V7.00016C11.0002 6.71429 11.1801 6.47044 11.4329 6.3757L7.00018 1.94297L2.56746 6.3757ZM5.00018 7.00016C5.00018 6.63197 5.29866 6.3335 5.66685 6.3335H8.33352C8.70171 6.3335 9.00018 6.63197 9.00018 7.00016V9.66683C9.00018 10.035 8.70171 10.3335 8.33352 10.3335H5.66685C5.29866 10.3335 5.00018 10.035 5.00018 9.66683V7.00016ZM6.33352 7.66683V9.00016H7.66685V7.66683H6.33352Z" fill="#C1C8CD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,3 @@
<svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.13911 0.938312C9.52665 0.551048 10.0521 0.333496 10.6 0.333496C11.1481 0.333496 11.6738 0.551241 12.0614 0.93883C12.449 1.32642 12.6667 1.8521 12.6667 2.40023C12.6667 2.94821 12.4491 3.47375 12.0617 3.86131C12.0616 3.86142 12.0618 3.8612 12.0617 3.86131L11.2191 4.70694C11.1957 4.74151 11.1687 4.77436 11.1381 4.80497C11.108 4.835 11.0759 4.86157 11.042 4.88467L6.47225 9.4708C6.34715 9.59634 6.17722 9.6669 6 9.6669H4C3.63181 9.6669 3.33333 9.36842 3.33333 9.00023V7.00023C3.33333 6.82301 3.4039 6.65308 3.52944 6.52799L8.11556 1.95821C8.13867 1.92438 8.16523 1.89219 8.19526 1.86216C8.22588 1.83155 8.25873 1.80453 8.29329 1.78112L9.1386 0.93883C9.13877 0.938657 9.13894 0.938485 9.13911 0.938312ZM8.67416 3.28387L4.66667 7.27707V8.33357H5.72316L9.71636 4.32608L8.67416 3.28387ZM10.6575 3.38158L9.61865 2.34274L10.0814 1.88164C10.2189 1.7441 10.4055 1.66683 10.6 1.66683C10.7945 1.66683 10.9811 1.7441 11.1186 1.88164C11.2561 2.01918 11.3334 2.20572 11.3334 2.40023C11.3334 2.59475 11.2561 2.78129 11.1186 2.91883L10.6575 3.38158ZM0.585786 3.58602C0.960859 3.21095 1.46957 3.00023 2 3.00023H2.66667C3.03486 3.00023 3.33333 3.29871 3.33333 3.6669C3.33333 4.03509 3.03486 4.33357 2.66667 4.33357H2C1.82319 4.33357 1.65362 4.40381 1.5286 4.52883C1.40357 4.65385 1.33333 4.82342 1.33333 5.00023V11.0002C1.33333 11.177 1.40357 11.3466 1.5286 11.4716C1.65362 11.5967 1.82319 11.6669 2 11.6669H8C8.17681 11.6669 8.34638 11.5967 8.4714 11.4716C8.59643 11.3466 8.66667 11.177 8.66667 11.0002V10.3336C8.66667 9.96538 8.96514 9.6669 9.33333 9.6669C9.70152 9.6669 10 9.96538 10 10.3336V11.0002C10 11.5307 9.78928 12.0394 9.41421 12.4144C9.03914 12.7895 8.53043 13.0002 8 13.0002H2C1.46957 13.0002 0.960859 12.7895 0.585786 12.4144C0.210714 12.0394 0 11.5307 0 11.0002V5.00023C0 4.4698 0.210714 3.96109 0.585786 3.58602Z" fill="#C1C8CD"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 2.16667C2.77899 2.16667 2.56702 2.25446 2.41074 2.41074C2.25446 2.56702 2.16667 2.77899 2.16667 3V13C2.16667 13.221 2.25446 13.433 2.41074 13.5893C2.56702 13.7455 2.77899 13.8333 3 13.8333H13C13.221 13.8333 13.433 13.7455 13.5893 13.5893C13.7455 13.433 13.8333 13.221 13.8333 13V5.01184L10.9882 2.16667H10.5V4.66667C10.5 5.1269 10.1269 5.5 9.66667 5.5H4.66667C4.20643 5.5 3.83333 5.1269 3.83333 4.66667V2.16667H3ZM3 0.5C2.33696 0.5 1.70107 0.763392 1.23223 1.23223C0.763392 1.70107 0.5 2.33696 0.5 3V13C0.5 13.663 0.763392 14.2989 1.23223 14.7678C1.70107 15.2366 2.33696 15.5 3 15.5H13C13.663 15.5 14.2989 15.2366 14.7678 14.7678C15.2366 14.2989 15.5 13.663 15.5 13V4.66667C15.5 4.44565 15.4122 4.23369 15.2559 4.07741L11.9226 0.744078C11.7663 0.587797 11.5543 0.5 11.3333 0.5H3ZM5.5 2.16667V3.83333H8.83333V2.16667H5.5ZM8 8.83333C7.53976 8.83333 7.16667 9.20643 7.16667 9.66667C7.16667 10.1269 7.53976 10.5 8 10.5C8.46024 10.5 8.83333 10.1269 8.83333 9.66667C8.83333 9.20643 8.46024 8.83333 8 8.83333ZM5.5 9.66667C5.5 8.28595 6.61929 7.16667 8 7.16667C9.38071 7.16667 10.5 8.28595 10.5 9.66667C10.5 11.0474 9.38071 12.1667 8 12.1667C6.61929 12.1667 5.5 11.0474 5.5 9.66667Z" fill="#FDFDFE"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,36 @@
<svg width="95" height="138" viewBox="0 0 95 138" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18.9551 47.7679L0.952026 59.6847L18.685 85.9162L36.6881 73.9994L18.9551 47.7679Z" fill="#F5F5F5"/>
<path d="M27.3856 72.7578L27.1803 74.0089L18.2301 72.7324L18.4355 71.4813L27.3856 72.7578Z" fill="#E0E0E0"/>
<path d="M22.8784 68.2949L24.1621 68.4774L22.7389 77.1973L21.4536 77.0131L22.8784 68.2949Z" fill="#E0E0E0"/>
<path d="M14.6342 57.2122C14.647 57.2313 12.8435 58.4475 10.6052 59.9288C8.36681 61.4102 6.54245 62.5962 6.52961 62.5771C6.51677 62.5581 8.32028 61.3419 10.5586 59.8605C12.797 58.3792 14.6213 57.1931 14.6342 57.2122Z" fill="#E0E0E0"/>
<path d="M21.2546 55.5673C21.2674 55.5863 18.2669 57.5948 14.554 60.051C10.8395 62.5088 7.81976 64.4856 7.80693 64.4665C7.79409 64.4475 10.793 62.4406 14.5075 59.9812C18.2204 57.5234 21.2418 55.5466 21.2546 55.5657V55.5673Z" fill="#E0E0E0"/>
<path d="M22.5816 57.5298C22.5945 57.5489 19.594 59.5573 15.8811 62.0136C12.1665 64.4714 9.14679 66.4481 9.13395 66.429C9.12112 66.41 12.12 64.4031 15.8345 61.9437C19.5474 59.4859 22.5688 57.5092 22.5816 57.5282V57.5298Z" fill="#E0E0E0"/>
<path d="M23.9085 59.494C23.9214 59.5131 20.9209 61.5216 17.208 63.9778C13.4934 66.4356 10.4737 68.4123 10.4609 68.3933C10.448 68.3742 13.4469 66.3673 17.1614 63.9079C20.8743 61.4501 23.8957 59.4734 23.9085 59.4924V59.494Z" fill="#E0E0E0"/>
<path d="M25.237 61.458C25.2499 61.477 22.2494 63.4855 18.5365 65.9417C14.8219 68.3995 11.8022 70.3763 11.7893 70.3572C11.7765 70.3382 14.7754 68.3313 18.4899 65.8719C22.2028 63.414 25.2242 61.4373 25.237 61.4564V61.458Z" fill="#E0E0E0"/>
<path d="M79.1554 65.3447L64.0356 75.3529L78.9281 97.3825L94.0478 87.3743L79.1554 65.3447Z" fill="#F5F5F5"/>
<path d="M86.2369 86.3359L86.0653 87.387L78.548 86.3153L78.7197 85.2642L86.2369 86.3359Z" fill="#E0E0E0"/>
<path d="M82.4503 82.5873L83.5301 82.7414L82.3331 90.064L81.2549 89.91L82.4503 82.5873Z" fill="#E0E0E0"/>
<path d="M75.5283 73.2799C75.5411 73.299 74.028 74.3231 72.1475 75.5663C70.267 76.8111 68.733 77.8034 68.7202 77.7843C68.7073 77.7653 70.2204 76.7412 72.1009 75.4964C73.9799 74.2532 75.5154 73.2593 75.5283 73.2783V73.2799Z" fill="#E0E0E0"/>
<path d="M81.088 71.8987C81.1009 71.9178 78.5833 73.6071 75.4641 75.6712C72.3449 77.7352 69.8065 79.3928 69.7936 79.3738C69.7808 79.3547 72.2983 77.667 75.4176 75.6013C78.5352 73.5373 81.0752 71.8797 81.088 71.8987Z" fill="#E0E0E0"/>
<path d="M82.2031 73.5469C82.216 73.5659 79.6984 75.2553 76.5792 77.3193C73.46 79.3834 70.9216 81.0409 70.9087 81.0219C70.8959 81.0028 73.4134 79.3151 76.5327 77.2494C79.6503 75.1854 82.1903 73.5278 82.2031 73.5469Z" fill="#E0E0E0"/>
<path d="M83.3167 75.1966C83.3295 75.2156 80.812 76.905 77.6927 78.9674C74.5735 81.0315 72.0351 82.6891 72.0223 82.67C72.0094 82.651 74.527 80.9632 77.6462 78.8976C80.7638 76.8335 83.3038 75.1759 83.3167 75.195V75.1966Z" fill="#E0E0E0"/>
<path d="M84.4318 76.8462C84.4446 76.8653 81.9271 78.5546 78.8078 80.6187C75.6886 82.6827 73.1502 84.3403 73.1374 84.3213C73.1245 84.3022 75.6421 82.6145 78.7613 80.5488C81.8789 78.4848 84.4189 76.8272 84.4318 76.8462Z" fill="#E0E0E0"/>
<path d="M55.2351 48.7312L38.7375 40L25.745 64.0376L42.2425 72.7688L55.2351 48.7312Z" fill="#F5F5F5"/>
<path d="M39.4197 65.2496L38.3736 65.6037L35.7164 58.3366L36.7626 57.981L39.4197 65.2496Z" fill="#E0E0E0"/>
<path d="M41.0242 60.0353L41.4061 61.0784L34.1119 63.5505L33.73 62.5074L41.0242 60.0353Z" fill="#E0E0E0"/>
<path d="M46.2069 49.3115C46.1941 49.3353 44.5206 48.4747 42.4699 47.3887C40.4177 46.3027 38.7667 45.4041 38.7795 45.3802C38.7923 45.3564 40.4659 46.217 42.5165 47.303C44.5671 48.389 46.2198 49.2877 46.2069 49.3115Z" fill="#E0E0E0"/>
<path d="M50.1654 53.6367C50.1525 53.6605 47.3831 52.2205 43.9814 50.42C40.5782 48.6195 37.8296 47.1397 37.8425 47.1159C37.8553 47.0921 40.6247 48.5321 44.028 50.3342C47.4296 52.1347 50.1782 53.6145 50.1654 53.6383V53.6367Z" fill="#E0E0E0"/>
<path d="M49.1931 55.4356C49.1802 55.4594 46.4108 54.0193 43.0091 52.2189C39.6059 50.4184 36.8573 48.9386 36.8702 48.9148C36.883 48.891 39.6524 50.331 43.0557 52.1331C46.4573 53.9336 49.2059 55.4134 49.1931 55.4372V55.4356Z" fill="#E0E0E0"/>
<path d="M48.2205 57.2345C48.2077 57.2584 45.4382 55.8183 42.0366 54.0178C38.6334 52.2173 35.8848 50.7375 35.8976 50.7137C35.9105 50.6899 38.6799 52.13 42.0831 53.9321C45.4848 55.7326 48.2334 57.2123 48.2205 57.2361V57.2345Z" fill="#E0E0E0"/>
<path d="M47.2482 59.0334C47.2354 59.0573 44.466 57.6172 41.0643 55.8167C37.6611 54.0162 34.9125 52.5364 34.9253 52.5126C34.9382 52.4888 37.7076 53.9289 41.1109 55.731C44.5125 57.5314 47.2611 59.0112 47.2482 59.035V59.0334Z" fill="#E0E0E0"/>
<path d="M36.624 104.734L30.9503 133.599L30.2074 137.377L68.0843 137.193C69.7835 137.185 71.2901 135.691 71.8373 133.473L78.5539 106.221C78.9695 104.537 78.0581 102.785 76.7681 102.785L38.4323 102.825C37.5851 102.825 36.8454 103.608 36.624 104.734Z" fill="#E4E7EB"/>
<g opacity="0.5">
<path d="M36.624 104.734L30.9503 133.599L30.2074 137.377L68.0843 137.193C69.7835 137.185 71.2901 135.691 71.8373 133.473L78.5539 106.221C78.9695 104.537 78.0581 102.785 76.7681 102.785L38.4323 102.825C37.5851 102.825 36.8454 103.608 36.624 104.734Z" fill="#E4E7EB"/>
</g>
<path d="M64.4516 131.644L60.4418 99.6657C60.2782 98.3558 59.2224 97.3778 57.9772 97.3825L49.1827 97.4111C48.5313 97.4127 47.9071 97.6874 47.445 98.1732L43.27 102.565L18.8119 102.692C17.3181 102.7 16.1692 104.099 16.3666 105.672L20.0907 135.228C20.256 136.536 21.3086 137.512 22.5521 137.509L65.1768 137.427C69.9247 137.524 70.3772 136.137 70.3772 136.137C64.9362 137.33 64.4516 131.649 64.4516 131.646V131.644Z" fill="#F3F4F6"/>
<g opacity="0.5">
<path d="M46.0731 125.244L44.6354 126.771L33.5641 116.914L35.0018 115.387L46.0731 125.244Z" fill="#6B7380"/>
<path d="M44.0321 115.049L45.6222 116.464L35.6034 127.109L34.0149 125.696L44.0321 115.049Z" fill="#6B7380"/>
</g>
<path d="M80.2866 137.585C80.2866 137.626 65.6063 137.661 47.5016 137.661C29.3968 137.661 14.7131 137.626 14.7131 137.585C14.7131 137.544 29.39 137.509 47.5016 137.509C65.6132 137.509 80.2866 137.544 80.2866 137.585Z" fill="#6B7380"/>
</svg>

After

Width:  |  Height:  |  Size: 6 KiB

View file

@ -35,7 +35,8 @@
"header": "HEADER",
"path": "PATH",
"query": "QUERY",
"requestBody": "REQUEST BODY"
"requestBody": "REQUEST BODY",
"page": "Page"
},
"errorBoundary": "Something went wrong.",
"viewer": "Sorry!. This app is under maintenance",
@ -202,7 +203,8 @@
"component": "Component",
"addHandler": "+ Add handler",
"addEventHandler": "+ Add event handler",
"emptyMessage": "This {{componentName}} doesn't have any event handlers"
"emptyMessage": "This {{componentName}} doesn't have any event handlers",
"page": "Page"
}
}
},

View file

@ -7,6 +7,9 @@
"": {
"version": "0.1.0",
"dependencies": {
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
"@react-google-maps/api": "^2.1.1",
"@sentry/react": "^7.12.0",
"@sentry/tracing": "^7.12.0",
@ -47,6 +50,7 @@
"react-beautiful-dnd": "^13.1.0",
"react-big-calendar": "^0.38.0",
"react-bootstrap": "^1.5.2",
"react-burger-menu": "^3.0.8",
"react-checkbox-tree": "^1.7.3",
"react-circular-progressbar": "^2.0.4",
"react-color": "^2.19.3",
@ -15438,6 +15442,75 @@
"node": ">=10.0.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/accessibility/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@dnd-kit/core": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.5.tgz",
"integrity": "sha512-3nL+Zy5cT+1XwsWdlXIvGIFvbuocMyB4NBxTN74DeBaBqeWdH9JsnKwQv7buZQgAHmAH+eIENfS1ginkvW6bCw==",
"dependencies": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@dnd-kit/sortable": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.1.tgz",
"integrity": "sha512-n77qAzJQtMMywu25sJzhz3gsHnDOUlEjTtnRl8A87rWIhnu32zuP+7zmFjwGgvqfXmRufqiHOSlH7JPC/tnJ8Q==",
"dependencies": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.0.4",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.0.tgz",
"integrity": "sha512-h65/pn2IPCCIWwdlR2BMLqRkDxpTEONA+HQW3n765HBijLYGyrnTCLa2YQt8VVjjSQD6EfFlTE6aS2Q/b6nb2g==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities/node_modules/tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
},
"node_modules/@egjs/children-differ": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@egjs/children-differ/-/children-differ-1.0.1.tgz",
@ -18795,7 +18868,7 @@
"node_modules/amdefine": {
"version": "1.0.1",
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true,
"devOptional": true,
"engines": {
"node": ">=0.4.2"
}
@ -18999,6 +19072,84 @@
"node": ">=0.8"
}
},
"node_modules/ast-transform": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz",
"integrity": "sha512-e/JfLiSoakfmL4wmTGPjv0HpTICVmxwXgYOB8x+mzozHL8v+dSfCbrJ8J8hJ0YBP0XcYu1aLZ6b/3TnxNK3P2A==",
"dependencies": {
"escodegen": "~1.2.0",
"esprima": "~1.0.4",
"through": "~2.3.4"
}
},
"node_modules/ast-transform/node_modules/escodegen": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz",
"integrity": "sha512-yLy3Cc+zAC0WSmoT2fig3J87TpQ8UaZGx8ahCAs9FL8qNbyV7CVyPKS74DG4bsHiL5ew9sxdYx131OkBQMFnvA==",
"dependencies": {
"esprima": "~1.0.4",
"estraverse": "~1.5.0",
"esutils": "~1.0.0"
},
"bin": {
"escodegen": "bin/escodegen.js",
"esgenerate": "bin/esgenerate.js"
},
"engines": {
"node": ">=0.4.0"
},
"optionalDependencies": {
"source-map": "~0.1.30"
}
},
"node_modules/ast-transform/node_modules/esprima": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz",
"integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA==",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ast-transform/node_modules/estraverse": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz",
"integrity": "sha512-FpCjJDfmo3vsc/1zKSeqR5k42tcIhxFIlvq+h9j0fO2q/h2uLKyweq7rYJ+0CoVvrGQOxIS5wyBrW/+vF58BUQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/ast-transform/node_modules/esutils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz",
"integrity": "sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ast-transform/node_modules/source-map": {
"version": "0.1.43",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
"integrity": "sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==",
"optional": true,
"dependencies": {
"amdefine": ">=0.0.4"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/ast-types": {
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz",
"integrity": "sha512-RIOpVnVlltB6PcBJ5BMLx+H+6JJ/zjDGU0t7f0L6c2M1dqcK92VQopLBlPQ9R80AVXelfqYgjcPLtHtDbNFg0Q==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
@ -19612,6 +19763,29 @@
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
"dev": true
},
"node_modules/browser-resolve": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",
"integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==",
"dependencies": {
"resolve": "1.1.7"
}
},
"node_modules/browser-resolve/node_modules/resolve": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
"integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg=="
},
"node_modules/browserify-optional": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz",
"integrity": "sha512-VrhjbZ+Ba5mDiSYEuPelekQMfTbhcA2DhLk2VQWqdcCROWeFqlTcXZ7yfRkXCIl8E+g4gINJYJiRB7WEtfomAQ==",
"dependencies": {
"ast-transform": "0.0.0",
"ast-types": "^0.7.0",
"browser-resolve": "^1.8.1"
}
},
"node_modules/browserslist": {
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz",
@ -22495,6 +22669,11 @@
"node": ">= 0.6"
}
},
"node_modules/eve": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/eve/-/eve-0.5.4.tgz",
"integrity": "sha512-aqprQ9MAOh1t66PrHxDFmMXPlgNO6Uv1uqvxmwjprQV50jaQ2RqO7O1neY4PJwC+hMnkyMDphu2AQPOPZdjQog=="
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
@ -30246,6 +30425,25 @@
"react-dom": ">=16.8.0"
}
},
"node_modules/react-burger-menu": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/react-burger-menu/-/react-burger-menu-3.0.8.tgz",
"integrity": "sha512-pkPHOUKKd5ClLg5OERIZLXtnYCO2Vxvti+BBIIVLNiD2xjCdgfkSt4TZ2IPUQBkYUY/id7Mq56YDgQ16p4kZog==",
"dependencies": {
"browserify-optional": "^1.0.0",
"classnames": "^2.2.6",
"eve": "~0.5.1",
"prop-types": "^15.7.2",
"snapsvg-cjs": "0.0.6"
},
"engines": {
"node": ">=4.0.0"
},
"peerDependencies": {
"react": ">=0.14.0",
"react-dom": ">=0.14.0"
}
},
"node_modules/react-checkbox-tree": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/react-checkbox-tree/-/react-checkbox-tree-1.7.3.tgz",
@ -31884,6 +32082,25 @@
"node": ">=8"
}
},
"node_modules/snapsvg": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/snapsvg/-/snapsvg-0.5.1.tgz",
"integrity": "sha512-CjwWYsL7+CCk1vCk9BBKGYS4WJVDfJAOMWU+Zhzf8wf6pAm/xT34wnpaMPAgcgCNkxuU6OkQPPd8wGuRCY9aNw==",
"dependencies": {
"eve": "~0.5.1"
}
},
"node_modules/snapsvg-cjs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/snapsvg-cjs/-/snapsvg-cjs-0.0.6.tgz",
"integrity": "sha512-7NNvoGrc3BQvWz5rWK1DsD5/Vni4STswz5B3JrBADboQWcN8OBVGjYVJFPT5JkUXb2iVnEflZANhufEpEcTHXw==",
"dependencies": {
"snapsvg": "0.5.1"
},
"peerDependencies": {
"eve": "~0.5.1"
}
},
"node_modules/sockjs": {
"version": "0.3.21",
"integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==",
@ -32656,6 +32873,11 @@
"integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==",
"dev": true
},
"node_modules/through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"node_modules/thunky": {
"version": "1.1.0",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",
@ -35481,6 +35703,69 @@
"integrity": "sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==",
"dev": true
},
"@dnd-kit/accessibility": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.0.1.tgz",
"integrity": "sha512-HXRrwS9YUYQO9lFRc/49uO/VICbM+O+ZRpFDe9Pd1rwVv2PCNkRiTZRdxrDgng/UkvdC3Re9r2vwPpXXrWeFzg==",
"requires": {
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@dnd-kit/core": {
"version": "6.0.5",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.0.5.tgz",
"integrity": "sha512-3nL+Zy5cT+1XwsWdlXIvGIFvbuocMyB4NBxTN74DeBaBqeWdH9JsnKwQv7buZQgAHmAH+eIENfS1ginkvW6bCw==",
"requires": {
"@dnd-kit/accessibility": "^3.0.0",
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@dnd-kit/sortable": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-7.0.1.tgz",
"integrity": "sha512-n77qAzJQtMMywu25sJzhz3gsHnDOUlEjTtnRl8A87rWIhnu32zuP+7zmFjwGgvqfXmRufqiHOSlH7JPC/tnJ8Q==",
"requires": {
"@dnd-kit/utilities": "^3.2.0",
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@dnd-kit/utilities": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.0.tgz",
"integrity": "sha512-h65/pn2IPCCIWwdlR2BMLqRkDxpTEONA+HQW3n765HBijLYGyrnTCLa2YQt8VVjjSQD6EfFlTE6aS2Q/b6nb2g==",
"requires": {
"tslib": "^2.0.0"
},
"dependencies": {
"tslib": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz",
"integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA=="
}
}
},
"@egjs/children-differ": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@egjs/children-differ/-/children-differ-1.0.1.tgz",
@ -48226,7 +48511,7 @@
"amdefine": {
"version": "1.0.1",
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
"dev": true
"devOptional": true
},
"ansi-escapes": {
"version": "4.3.2",
@ -48370,6 +48655,58 @@
"integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
"dev": true
},
"ast-transform": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz",
"integrity": "sha512-e/JfLiSoakfmL4wmTGPjv0HpTICVmxwXgYOB8x+mzozHL8v+dSfCbrJ8J8hJ0YBP0XcYu1aLZ6b/3TnxNK3P2A==",
"requires": {
"escodegen": "~1.2.0",
"esprima": "~1.0.4",
"through": "~2.3.4"
},
"dependencies": {
"escodegen": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz",
"integrity": "sha512-yLy3Cc+zAC0WSmoT2fig3J87TpQ8UaZGx8ahCAs9FL8qNbyV7CVyPKS74DG4bsHiL5ew9sxdYx131OkBQMFnvA==",
"requires": {
"esprima": "~1.0.4",
"estraverse": "~1.5.0",
"esutils": "~1.0.0",
"source-map": "~0.1.30"
}
},
"esprima": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz",
"integrity": "sha512-rp5dMKN8zEs9dfi9g0X1ClLmV//WRyk/R15mppFNICIFRG5P92VP7Z04p8pk++gABo9W2tY+kHyu6P1mEHgmTA=="
},
"estraverse": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz",
"integrity": "sha512-FpCjJDfmo3vsc/1zKSeqR5k42tcIhxFIlvq+h9j0fO2q/h2uLKyweq7rYJ+0CoVvrGQOxIS5wyBrW/+vF58BUQ=="
},
"esutils": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz",
"integrity": "sha512-x/iYH53X3quDwfHRz4y8rn4XcEwwCJeWsul9pF1zldMbGtgOtMNBEOuYWwB1EQlK2LRa1fev3YAgym/RElp5Cg=="
},
"source-map": {
"version": "0.1.43",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz",
"integrity": "sha512-VtCvB9SIQhk3aF6h+N85EaqIaBFIAfZ9Cu+NJHHVvc8BbEcnvDcFw6sqQ2dQrT6SlOrZq3tIvyD9+EGq/lJryQ==",
"optional": true,
"requires": {
"amdefine": ">=0.0.4"
}
}
}
},
"ast-types": {
"version": "0.7.8",
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz",
"integrity": "sha512-RIOpVnVlltB6PcBJ5BMLx+H+6JJ/zjDGU0t7f0L6c2M1dqcK92VQopLBlPQ9R80AVXelfqYgjcPLtHtDbNFg0Q=="
},
"async": {
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
@ -48847,6 +49184,31 @@
"integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==",
"dev": true
},
"browser-resolve": {
"version": "1.11.3",
"resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",
"integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==",
"requires": {
"resolve": "1.1.7"
},
"dependencies": {
"resolve": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
"integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg=="
}
}
},
"browserify-optional": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz",
"integrity": "sha512-VrhjbZ+Ba5mDiSYEuPelekQMfTbhcA2DhLk2VQWqdcCROWeFqlTcXZ7yfRkXCIl8E+g4gINJYJiRB7WEtfomAQ==",
"requires": {
"ast-transform": "0.0.0",
"ast-types": "^0.7.0",
"browser-resolve": "^1.8.1"
}
},
"browserslist": {
"version": "4.21.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.3.tgz",
@ -50900,6 +51262,11 @@
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"dev": true
},
"eve": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/eve/-/eve-0.5.4.tgz",
"integrity": "sha512-aqprQ9MAOh1t66PrHxDFmMXPlgNO6Uv1uqvxmwjprQV50jaQ2RqO7O1neY4PJwC+hMnkyMDphu2AQPOPZdjQog=="
},
"eventemitter3": {
"version": "4.0.7",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
@ -56679,6 +57046,18 @@
"warning": "^4.0.3"
}
},
"react-burger-menu": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/react-burger-menu/-/react-burger-menu-3.0.8.tgz",
"integrity": "sha512-pkPHOUKKd5ClLg5OERIZLXtnYCO2Vxvti+BBIIVLNiD2xjCdgfkSt4TZ2IPUQBkYUY/id7Mq56YDgQ16p4kZog==",
"requires": {
"browserify-optional": "^1.0.0",
"classnames": "^2.2.6",
"eve": "~0.5.1",
"prop-types": "^15.7.2",
"snapsvg-cjs": "0.0.6"
}
},
"react-checkbox-tree": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/react-checkbox-tree/-/react-checkbox-tree-1.7.3.tgz",
@ -57913,6 +58292,22 @@
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true
},
"snapsvg": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/snapsvg/-/snapsvg-0.5.1.tgz",
"integrity": "sha512-CjwWYsL7+CCk1vCk9BBKGYS4WJVDfJAOMWU+Zhzf8wf6pAm/xT34wnpaMPAgcgCNkxuU6OkQPPd8wGuRCY9aNw==",
"requires": {
"eve": "~0.5.1"
}
},
"snapsvg-cjs": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/snapsvg-cjs/-/snapsvg-cjs-0.0.6.tgz",
"integrity": "sha512-7NNvoGrc3BQvWz5rWK1DsD5/Vni4STswz5B3JrBADboQWcN8OBVGjYVJFPT5JkUXb2iVnEflZANhufEpEcTHXw==",
"requires": {
"snapsvg": "0.5.1"
}
},
"sockjs": {
"version": "0.3.21",
"integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==",
@ -58491,6 +58886,11 @@
"integrity": "sha512-8hmiGIJMDlwjg7dlJ4yKGLK8EsYqKgPWbG3b4wjJddKNwc7N7Dpn08Df4szr/sZdMVeOstrdYSsqzX6BYbcB+w==",
"dev": true
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
"integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
},
"thunky": {
"version": "1.1.0",
"integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==",

View file

@ -3,6 +3,9 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@dnd-kit/core": "^6.0.5",
"@dnd-kit/sortable": "^7.0.1",
"@dnd-kit/utilities": "^3.2.0",
"@react-google-maps/api": "^2.1.1",
"@sentry/react": "^7.12.0",
"@sentry/tracing": "^7.12.0",
@ -43,6 +46,7 @@
"react-beautiful-dnd": "^13.1.0",
"react-big-calendar": "^0.38.0",
"react-bootstrap": "^1.5.2",
"react-burger-menu": "^3.0.8",
"react-checkbox-tree": "^1.7.3",
"react-circular-progressbar": "^2.0.4",
"react-color": "^2.19.3",

View file

@ -189,21 +189,21 @@ class App extends React.Component {
<Route path="/confirm-invite" component={OrganizationInvitationPage} />
<PrivateRoute
exact
path="/apps/:id"
path="/apps/:id/:pageHandle?"
component={AppLoader}
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}
/>
<PrivateRoute
exact
path="/applications/:id/versions/:versionId"
path="/applications/:id/versions/:versionId/:pageHandle?"
component={Viewer}
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}
/>
<PrivateRoute
exact
path="/applications/:slug"
path="/applications/:slug/:pageHandle?"
component={Viewer}
switchDarkMode={this.switchDarkMode}
darkMode={darkMode}

View file

@ -83,6 +83,27 @@ export const ActionTypes = [
id: 'unset-custom-variable',
options: [{ name: 'key', type: 'code', default: '' }],
},
{
name: 'Switch page',
id: 'switch-page',
options: [{ name: 'page', type: 'text', default: '' }],
},
{
name: 'Set page variable',
id: 'set-page-variable',
options: [
{ name: 'key', type: 'code', default: '' },
{ name: 'value', type: 'code', default: '' },
],
},
{
name: 'Unset page variable',
id: 'unset-page-variable',
options: [
{ name: 'key', type: 'code', default: '' },
{ name: 'value', type: 'code', default: '' },
],
},
{
name: 'Control component',
id: 'control-component',

View file

@ -196,13 +196,17 @@ export const Box = function Box({
const { variablesExposedForPreview, exposeToCodeHinter } = useContext(EditorContext) || {};
useEffect(() => {
const currentPage = currentState?.page;
const componentName = getComponentName(currentState, id);
const errorLog = Object.fromEntries(
[...propertyErrors, ...styleErrors, ...generalPropertiesErrors, ...generalStylesErrors].map((error) => [
`${componentName} - ${error.property}`,
{
page: currentPage,
type: 'component',
kind: 'component',
strace: 'page_level',
data: { message: `${error.message}`, status: true },
resolvedProperties: resolvedProperties,
effectiveProperties: validatedProperties,

View file

@ -49,8 +49,8 @@ export const BoxShadow = ({ value, onChange, forceCodeBox, cyLabel }) => {
}, []);
useEffect(() => {
onChange(Object.values(debouncedShadow).join('px '));
if (boxShadow !== debouncedShadow) {
onChange(Object.values(debouncedShadow).join('px '));
setBoxShadow(debouncedShadow);
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View file

@ -8,7 +8,7 @@ import { commentsService } from '@/_services';
import useRouter from '@/_hooks/use-router';
const Comments = ({ newThread = {}, appVersionsId, socket, canvasWidth }) => {
const Comments = ({ newThread = {}, appVersionsId, socket, canvasWidth, currentPageId }) => {
const [threads, setThreads] = React.useState([]);
const router = useRouter();
@ -37,20 +37,22 @@ const Comments = ({ newThread = {}, appVersionsId, socket, canvasWidth }) => {
if (isEmpty(threads)) return null;
return threads.map((thread) => {
const { id } = thread;
return (
<Comment
key={id}
appVersionsId={appVersionsId}
fetchThreads={fetchData}
socket={socket}
threadId={id}
canvasWidth={canvasWidth}
{...thread}
/>
);
});
return threads
.filter((thread) => thread.pageId === currentPageId)
.map((thread) => {
const { id } = thread;
return (
<Comment
key={id}
appVersionsId={appVersionsId}
fetchThreads={fetchData}
socket={socket}
threadId={id}
canvasWidth={canvasWidth}
{...thread}
/>
);
});
};
export default Comments;

View file

@ -45,6 +45,7 @@ export const Container = ({
hoveredComponent,
sideBarDebugger,
dataQueries,
currentPageId,
}) => {
const styles = {
width: currentLayout === 'mobile' ? deviceWindowWidth : '100%',
@ -54,7 +55,7 @@ export const Container = ({
backgroundSize: `${canvasWidth / 43}px 10px`,
};
const components = appDefinition.components;
const components = appDefinition.pages[currentPageId]?.components ?? {};
const [boxes, setBoxes] = useState(components);
const [isDragging, setIsDragging] = useState(false);
@ -75,7 +76,13 @@ export const Container = ({
if (isContainerFocused) {
navigator.clipboard.readText().then((cliptext) => {
try {
addComponents(appDefinition, appDefinitionChanged, focusedParentIdRef.current, JSON.parse(cliptext));
addComponents(
currentPageId,
appDefinition,
appDefinitionChanged,
focusedParentIdRef.current,
JSON.parse(cliptext)
);
} catch (err) {
console.log(err);
}
@ -108,7 +115,7 @@ export const Container = ({
useEffect(() => {
setBoxes(components);
}, [components]);
}, [JSON.stringify(components)]);
const moveBox = useCallback(
(id, layouts) => {
@ -131,7 +138,19 @@ export const Container = ({
firstUpdate.current = false;
return;
}
appDefinitionChanged({ ...appDefinition, components: boxes });
const newDefinition = {
...appDefinition,
pages: {
...appDefinition.pages,
[currentPageId]: {
...appDefinition.pages[currentPageId],
components: boxes,
},
},
};
appDefinitionChanged(newDefinition);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [boxes]);
@ -293,7 +312,7 @@ export const Container = ({
function paramUpdated(id, param, value) {
if (Object.keys(value).length > 0) {
setBoxes(
setBoxes((boxes) =>
update(boxes, {
[id]: {
$merge: {
@ -335,6 +354,7 @@ export const Container = ({
x: x,
y: e.nativeEvent.offsetY,
appVersionsId,
pageId: currentPageId,
});
// Remove the temporary loader preview
@ -379,6 +399,7 @@ export const Container = ({
x,
y: y - 130,
appVersionsId,
pageId: currentPageId,
});
// Remove the temporary loader preview
@ -420,7 +441,13 @@ export const Container = ({
>
{config.COMMENT_FEATURE_ENABLE && showComments && (
<>
<Comments socket={socket} newThread={newThread} appVersionsId={appVersionsId} canvasWidth={canvasWidth} />
<Comments
socket={socket}
newThread={newThread}
appVersionsId={appVersionsId}
canvasWidth={canvasWidth}
currentPageId={currentPageId}
/>
{commentsPreviewList.map((previewComment, index) => (
<div
key={index}
@ -507,6 +534,7 @@ export const Container = ({
sideBarDebugger,
dataQueries,
addDefaultChildren,
currentPageId,
}}
/>
);

View file

@ -315,6 +315,7 @@ export const DraggableBox = function DraggableBox({
removeComponent={removeComponent}
sideBarDebugger={sideBarDebugger}
customResolvables={customResolvables}
containerProps={containerProps}
/>
</ErrorBoundary>
</div>

View file

@ -11,7 +11,7 @@ import {
} from '@/_services';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { defaults, cloneDeep, isEqual, isEmpty, debounce } from 'lodash';
import { defaults, cloneDeep, isEqual, isEmpty, debounce, omit } from 'lodash';
import { Container } from './Container';
import { EditorKeyHooks } from './EditorKeyHooks';
import { CustomDragLayer } from './CustomDragLayer';
@ -72,6 +72,8 @@ class EditorComponent extends React.Component {
const appId = this.props.match.params.id;
const pageHandle = this.props.match.params.pageHandle;
const currentUser = authenticationService.currentUserValue;
const { socket } = createWebsocketConnection(appId);
@ -88,12 +90,23 @@ class EditorComponent extends React.Component {
};
}
const defaultPageId = uuid();
this.defaultDefinition = {
components: {},
showViewerNavigation: true,
homePageId: defaultPageId,
pages: {
[defaultPageId]: {
components: {},
handle: 'home',
name: 'Home',
},
},
globalSettings: {
hideHeader: false,
appInMaintenance: false,
canvasMaxWidth: 1292,
canvasMaxWidthType: 'px',
canvasMaxHeight: 2400,
canvasBackgroundColor: props.darkMode ? '#2f3c4c' : '#edeff5',
backgroundFxQuery: '',
@ -134,6 +147,10 @@ class EditorComponent extends React.Component {
variables: {},
client: {},
server: {},
page: {
handle: pageHandle,
variables: {},
},
},
apps: [],
dataQueriesDefaultText: "You haven't created queries yet.",
@ -147,6 +164,8 @@ class EditorComponent extends React.Component {
isUnsavedQueriesAvailable: false,
selectionInProgress: false,
scrollOptions: {},
currentPageId: defaultPageId,
pages: {},
};
this.autoSave = debounce(this.saveEditingVersion, 3000);
@ -159,7 +178,7 @@ class EditorComponent extends React.Component {
componentDidMount() {
this.fetchApps(0);
this.fetchApp();
this.fetchApp(this.props.match.params.pageHandle);
this.fetchOrgEnvironmentVariables();
this.initComponentVersioning();
this.initRealtimeSave();
@ -215,7 +234,7 @@ class EditorComponent extends React.Component {
componentDidUpdate(prevProps, prevState) {
if (!isEqual(prevState.appDefinition, this.state.appDefinition)) {
computeComponentState(this, this.state.appDefinition.components);
computeComponentState(this, this.state.appDefinition.pages[this.state.currentPageId]?.components);
}
}
@ -367,11 +386,16 @@ class EditorComponent extends React.Component {
);
};
fetchApp = () => {
fetchApp = (startingPageHandle) => {
const appId = this.props.match.params.id;
appService.getApp(appId).then(async (data) => {
const dataDefinition = defaults(data.definition, this.defaultDefinition);
let dataDefinition = defaults(data.definition, this.defaultDefinition);
const pages = Object.entries(dataDefinition.pages).map(([pageId, page]) => ({ id: pageId, ...page }));
const startingPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id;
const homePageId = startingPageId ?? dataDefinition.homePageId;
this.setState(
{
app: data,
@ -379,17 +403,31 @@ class EditorComponent extends React.Component {
editingVersion: data.editing_version,
appDefinition: dataDefinition,
slug: data.slug,
currentPageId: homePageId,
currentState: {
...this.state.currentState,
page: {
handle: dataDefinition.pages[homePageId]?.handle,
name: dataDefinition.pages[homePageId]?.name,
id: homePageId,
variables: {},
},
},
},
async () => {
if (isEmpty(this.state.editingVersion)) await this.createInitVersion(appId);
computeComponentState(this, this.state.appDefinition.components).then(() => {
// TODO: Check if this runQueries is required
computeComponentState(this, this.state.appDefinition.pages[homePageId]?.components ?? {}).then(() => {
this.runQueries(data.data_queries);
});
this.setWindowTitle(data.name);
this.setState({
showComments: !!queryString.parse(this.props.location.search).threadId,
});
for (const event of dataDefinition.pages[homePageId]?.events ?? []) {
await this.handleEvent(event.eventId, event);
}
}
);
@ -414,6 +452,7 @@ class EditorComponent extends React.Component {
this.appDefinitionChanged(defaults(version.definition, this.defaultDefinition), {
skipAutoSave: true,
skipYmapUpdate: true,
versionChanged: true,
});
this.setState({
editingVersion: version,
@ -546,22 +585,41 @@ class EditorComponent extends React.Component {
};
appDefinitionChanged = (newDefinition, opts = {}) => {
let currentPageId = this.state.currentPageId;
if (isEqual(this.state.appDefinition, newDefinition)) return;
if (config.ENABLE_MULTIPLAYER_EDITING && !opts.skipYmapUpdate) {
this.props.ymap?.set('appDef', { newDefinition, editingVersionId: this.state.editingVersion?.id });
}
if (opts?.versionChanged) {
currentPageId = newDefinition.homePageId;
this.setState(
{
isSaving: true,
currentPageId: currentPageId,
appDefinition: newDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
if (!opts.skipAutoSave) this.autoSave();
this.switchPage(currentPageId);
}
);
return;
}
produce(
this.state.appDefinition,
(draft) => {
draft.components = newDefinition.components;
draft.pages[currentPageId].components = newDefinition.pages[currentPageId]?.components ?? {};
},
this.handleAddPatch
);
this.setState({ isSaving: true, appDefinition: newDefinition, appDefinitionLocalVersion: uuid() }, () => {
if (!opts.skipAutoSave) this.autoSave();
});
computeComponentState(this, newDefinition.components);
computeComponentState(this, newDefinition.pages[currentPageId]?.components ?? {});
};
handleInspectorView = () => {
@ -577,7 +635,7 @@ class EditorComponent extends React.Component {
let newDefinition = cloneDeep(this.state.appDefinition);
const selectedComponents = this.state?.selectedComponents;
removeSelectedComponent(newDefinition, selectedComponents);
removeSelectedComponent(this.state.currentPageId, newDefinition, selectedComponents);
const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown';
if (platform.toLowerCase().indexOf('mac') > -1) {
toast('Selected components deleted! (⌘ + Z to undo)', {
@ -598,27 +656,28 @@ class EditorComponent extends React.Component {
};
removeComponent = (component) => {
const currentPageId = this.state.currentPageId;
if (!this.isVersionReleased()) {
let newDefinition = cloneDeep(this.state.appDefinition);
// Delete child components when parent is deleted
let childComponents = [];
if (newDefinition.components[component.id].component.component === 'Tabs') {
childComponents = Object.keys(newDefinition.components).filter((key) =>
newDefinition.components[key].parent?.startsWith(component.id)
if (newDefinition.pages[currentPageId].components?.[component.id].component.component === 'Tabs') {
childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter((key) =>
newDefinition.pages[currentPageId].components[key].parent?.startsWith(component.id)
);
} else {
childComponents = Object.keys(newDefinition.components).filter(
(key) => newDefinition.components[key].parent === component.id
childComponents = Object.keys(newDefinition.pages[currentPageId].components).filter(
(key) => newDefinition.pages[currentPageId].components[key].parent === component.id
);
}
childComponents.forEach((componentId) => {
delete newDefinition.components[componentId];
delete newDefinition.pages[currentPageId].components[componentId];
});
delete newDefinition.components[component.id];
delete newDefinition.pages[currentPageId].components[component.id];
const platform = navigator?.userAgentData?.platform || navigator?.platform || 'unknown';
if (platform.toLowerCase().indexOf('mac') > -1) {
toast('Component deleted! (⌘ + Z to undo)', {
@ -640,24 +699,25 @@ class EditorComponent extends React.Component {
componentDefinitionChanged = (componentDefinition) => {
let _self = this;
const currentPageId = this.state.currentPageId;
if (this.state.appDefinition?.components[componentDefinition.id]) {
if (this.state.appDefinition?.pages[currentPageId].components[componentDefinition.id]) {
const newDefinition = {
appDefinition: produce(this.state.appDefinition, (draft) => {
draft.components[componentDefinition.id].component = componentDefinition.component;
draft.pages[currentPageId].components[componentDefinition.id].component = componentDefinition.component;
}),
};
produce(
this.state.appDefinition,
(draft) => {
draft.components[componentDefinition.id].component = componentDefinition.component;
draft.pages[currentPageId].components[componentDefinition.id].component = componentDefinition.component;
},
this.handleAddPatch
);
setStateAsync(_self, newDefinition).then(() => {
computeComponentState(_self, _self.state.appDefinition.components);
this.setState({ isSaving: true });
computeComponentState(_self, _self.state.appDefinition.pages[currentPageId].components);
this.setState({ isSaving: true, appDefinitionLocalVersion: uuid() });
this.autoSave();
this.props.ymap?.set('appDef', {
newDefinition: newDefinition.appDefinition,
@ -676,7 +736,7 @@ class EditorComponent extends React.Component {
moveComponents = (direction) => {
let appDefinition = JSON.parse(JSON.stringify(this.state.appDefinition));
let newComponents = appDefinition.components;
let newComponents = appDefinition.pages[this.state.currentPageId].components;
for (const selectedComponent of this.state.selectedComponents) {
newComponents = produce(newComponents, (draft) => {
@ -704,7 +764,7 @@ class EditorComponent extends React.Component {
draft[selectedComponent.id].layouts[this.state.currentLayout].left = left;
});
}
appDefinition.components = newComponents;
appDefinition.pages[this.state.currentPageId].components = newComponents;
this.appDefinitionChanged(appDefinition);
};
@ -1157,10 +1217,11 @@ class EditorComponent extends React.Component {
};
onAreaSelectionEnd = (e) => {
const currentPageId = this.state.currentPageId;
this.setState({ selectionInProgress: false });
e.selected.forEach((el, index) => {
const id = el.getAttribute('widgetid');
const component = this.state.appDefinition.components[id].component;
const component = this.state.appDefinition.pages[currentPageId].components[id].component;
const isMultiSelect = e.inputEvent.shiftKey || (!e.isClick && index != 0);
this.setSelectedComponent(id, component, isMultiSelect);
});
@ -1186,6 +1247,348 @@ class EditorComponent extends React.Component {
this.state.selectionInProgress && this.setState({ selectionInProgress: false });
};
addNewPage = ({ name, handle }) => {
// check for unique page handles
const pageExists = Object.values(this.state.appDefinition.pages).some((page) => page.handle === handle);
if (pageExists) {
toast.error('Page with same handle already exists');
return;
}
const newAppDefinition = {
...this.state.appDefinition,
pages: {
...this.state.appDefinition.pages,
[uuid()]: {
name,
handle,
components: {},
},
},
};
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
const newPageId = cloneDeep(Object.keys(newAppDefinition.pages)).pop();
this.switchPage(newPageId);
this.autoSave();
}
);
};
removePage = (pageId, isHomePage = false) => {
if (Object.keys(this.state.appDefinition.pages).length === 1) {
toast.error('You cannot delete the only page in your app.');
return;
}
const toBeDeletedPage = this.state.appDefinition.pages[pageId];
const newAppDefinition = {
...this.state.appDefinition,
pages: omit(this.state.appDefinition.pages, pageId),
};
const newCurrentPageId = isHomePage
? Object.keys(this.state.appDefinition.pages)[0]
: this.state.appDefinition.homePageId;
this.setState(
{
currentPageId: newCurrentPageId,
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
toast.success(`${toBeDeletedPage.name} page deleted.`);
this.switchPage(newCurrentPageId);
this.autoSave();
}
);
};
updateHomePage = (pageId) => {
this.setState(
{
isSaving: true,
appDefinition: {
...this.state.appDefinition,
homePageId: pageId,
},
appDefinitionLocalVersion: uuid(),
},
() => {
this.autoSave();
}
);
};
clonePage = (pageId) => {
const currentPage = this.state.appDefinition.pages[pageId];
const newPageId = uuid();
let newPageName = `${currentPage.name} (copy)`;
let newPageHandle = `${currentPage.handle}-copy`;
let i = 1;
while (Object.values(this.state.appDefinition.pages).some((page) => page.handle === newPageHandle)) {
newPageName = `${currentPage.name} (copy ${i})`;
newPageHandle = `${currentPage.handle}-copy-${i}`;
i++;
}
const newPage = {
...cloneDeep(currentPage),
name: newPageName,
handle: newPageHandle,
};
const newAppDefinition = {
...this.state.appDefinition,
pages: {
...this.state.appDefinition.pages,
[newPageId]: newPage,
},
};
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
this.autoSave();
}
);
};
updatePageHandle = (pageId, newHandle) => {
const pageExists = Object.values(this.state.appDefinition.pages).some((page) => page.handle === newHandle);
if (pageExists) {
toast.error('Page with same handle already exists');
return;
}
this.setState(
{
isSaving: true,
appDefinition: {
...this.state.appDefinition,
pages: {
...this.state.appDefinition.pages,
[pageId]: {
...this.state.appDefinition.pages[pageId],
handle: newHandle,
},
},
},
appDefinitionLocalVersion: uuid(),
},
() => {
toast.success('Page handle updated successfully');
this.switchPage(pageId);
this.autoSave();
}
);
};
updateOnPageLoadEvents = (pageId, events) => {
this.setState(
{
isSaving: true,
appDefinition: {
...this.state.appDefinition,
pages: {
...this.state.appDefinition.pages,
[pageId]: {
...this.state.appDefinition.pages[pageId],
events,
},
},
},
appDefinitionLocalVersion: uuid(),
},
() => {
this.autoSave();
}
);
};
showHideViewerNavigation = () => {
const newAppDefinition = {
...this.state.appDefinition,
showViewerNavigation: !this.state.appDefinition.showViewerNavigation,
};
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
() => this.autoSave()
);
};
renamePage = (pageId, newName) => {
const newAppDefinition = {
...this.state.appDefinition,
pages: {
...this.state.appDefinition.pages,
[pageId]: {
...this.state.appDefinition.pages[pageId],
name: newName,
},
},
};
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
this.autoSave();
}
);
};
hidePage = (pageId) => {
const newAppDefinition = {
...this.state.appDefinition,
pages: {
...this.state.appDefinition.pages,
[pageId]: {
...this.state.appDefinition.pages[pageId],
hidden: true,
},
},
};
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
this.autoSave();
}
);
};
unHidePage = (pageId) => {
const newAppDefinition = {
...this.state.appDefinition,
pages: {
...this.state.appDefinition.pages,
[pageId]: {
...this.state.appDefinition.pages[pageId],
hidden: false,
},
},
};
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
this.autoSave();
}
);
};
switchPage = (pageId, queryParams = []) => {
const { name, handle, events } = this.state.appDefinition.pages[pageId];
const currentPageId = this.state.currentPageId;
if (!name || !handle) return;
const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&');
this.props.history.push(`/apps/${this.state.appId}/${handle}?${queryParamsString}`);
const { globals: existingGlobals } = this.state.currentState;
const page = {
...this.state.currentState.page,
name,
handle,
variables: this.state.pages?.[pageId]?.variables ?? {},
};
const globals = {
...existingGlobals,
urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))),
};
this.setState(
{
pages: {
...this.state.pages,
[currentPageId]: {
...(this.state.pages?.[currentPageId] ?? {}),
variables: {
...(this.state.currentState?.page?.variables ?? {}),
},
},
},
currentState: {
...this.state.currentState,
globals,
page,
},
currentPageId: pageId,
},
() => {
computeComponentState(this, this.state.appDefinition.pages[pageId]?.components ?? {}).then(async () => {
for (const event of events ?? []) {
await this.handleEvent(event.eventId, event);
}
});
}
);
};
updateOnSortingPages = (newSortedPages) => {
const pagesObj = newSortedPages.reduce((acc, page) => {
acc[page.id] = this.state.appDefinition.pages[page.id];
return acc;
}, {});
const newAppDefinition = {
...this.state.appDefinition,
pages: pagesObj,
};
this.setState(
{
isSaving: true,
appDefinition: newAppDefinition,
appDefinitionLocalVersion: uuid(),
},
() => {
this.autoSave();
}
);
};
getPagesWithIds = () => {
return Object.entries(this.state.appDefinition.pages).map(([id, page]) => ({ ...page, id }));
};
render() {
const {
currentSidebarTab,
@ -1221,7 +1624,9 @@ class EditorComponent extends React.Component {
queryConfirmationList,
} = this.state;
const appVersionPreviewLink = editingVersion ? `/applications/${app.id}/versions/${editingVersion.id}` : '';
const appVersionPreviewLink = editingVersion
? `/applications/${app.id}/versions/${editingVersion.id}/${this.state.currentState.page.handle}`
: '';
return (
<div className="editor wrapper">
@ -1355,9 +1760,12 @@ class EditorComponent extends React.Component {
currentState={currentState}
debuggerActions={this.sideBarDebugger}
appDefinition={{
components: appDefinition.components,
components: appDefinition.pages[this.state.currentPageId]?.components ?? {},
queries: dataQueries,
selectedComponent: selectedComponents ? selectedComponents[selectedComponents.length - 1] : {},
pages: this.state.appDefinition.pages,
homePageId: this.state.appDefinition.homePageId,
showViewerNavigation: this.state.appDefinition.showViewerNavigation,
}}
setSelectedComponent={this.setSelectedComponent}
removeComponent={this.removeComponent}
@ -1367,6 +1775,21 @@ class EditorComponent extends React.Component {
ref={this.dataSourceModalRef}
isSaving={this.state.isSaving}
isUnsavedQueriesAvailable={this.state.isUnsavedQueriesAvailable}
currentPageId={this.state.currentPageId}
addNewPage={this.addNewPage}
switchPage={this.switchPage}
deletePage={this.removePage}
renamePage={this.renamePage}
clonePage={this.clonePage}
hidePage={this.hidePage}
unHidePage={this.unHidePage}
updateHomePage={this.updateHomePage}
updatePageHandle={this.updatePageHandle}
updateOnPageLoadEvents={this.updateOnPageLoadEvents}
showHideViewerNavigationControls={this.showHideViewerNavigation}
updateOnSortingPages={this.updateOnSortingPages}
apps={apps}
dataQueries={dataQueries}
/>
{!showComments && (
<Selecto
@ -1407,13 +1830,18 @@ class EditorComponent extends React.Component {
style={{
width: currentLayout === 'desktop' ? '100%' : '450px',
minHeight: +this.state.appDefinition.globalSettings.canvasMaxHeight,
maxWidth: +this.state.appDefinition.globalSettings.canvasMaxWidth,
maxWidth:
+this.state.appDefinition.globalSettings.canvasMaxWidth +
this.state.appDefinition.globalSettings.canvasMaxWidthType,
maxHeight: +this.state.appDefinition.globalSettings.canvasMaxHeight,
backgroundColor: this.computeCanvasBackgroundColor(),
}}
>
{config.ENABLE_MULTIPLAYER_EDITING && (
<RealtimeCursors editingVersionId={this.state?.editingVersion?.id} />
<RealtimeCursors
editingVersionId={this.state?.editingVersion?.id}
editingPageId={this.state.currentPageId}
/>
)}
{defaultComponentStateComputed && (
<>
@ -1446,6 +1874,7 @@ class EditorComponent extends React.Component {
hoveredComponent={hoveredComponent}
sideBarDebugger={this.sideBarDebugger}
dataQueries={dataQueries}
currentPageId={this.state.currentPageId}
/>
<CustomDragLayer
snapToGrid={true}
@ -1611,7 +2040,7 @@ class EditorComponent extends React.Component {
currentState={currentState}
darkMode={this.props.darkMode}
apps={apps}
allComponents={appDefinition.components}
allComponents={appDefinition.pages[this.state.currentPageId]?.components ?? {}}
isSourceSelected={this.state.isSourceSelected}
isQueryPaneDragging={this.state.isQueryPaneDragging}
runQuery={this.runQuery}
@ -1694,8 +2123,8 @@ class EditorComponent extends React.Component {
{currentSidebarTab === 1 && (
<div className="pages-container">
{selectedComponents.length === 1 &&
!isEmpty(appDefinition.components) &&
!isEmpty(appDefinition.components[selectedComponents[0].id]) ? (
!isEmpty(appDefinition.pages[this.state.currentPageId]?.components) &&
!isEmpty(appDefinition.pages[this.state.currentPageId]?.components[selectedComponents[0].id]) ? (
<Inspector
moveComponents={this.moveComponents}
componentDefinitionChanged={this.componentDefinitionChanged}
@ -1703,13 +2132,14 @@ class EditorComponent extends React.Component {
removeComponent={this.removeComponent}
selectedComponentId={selectedComponents[0].id}
currentState={currentState}
allComponents={appDefinition.components}
allComponents={appDefinition.pages[this.state.currentPageId]?.components}
key={selectedComponents[0].id}
switchSidebarTab={this.switchSidebarTab}
apps={apps}
darkMode={this.props.darkMode}
handleEditorEscapeKeyPress={this.handleEditorEscapeKeyPress}
appDefinitionLocalVersion={this.state.appDefinitionLocalVersion}
pages={this.getPagesWithIds()}
></Inspector>
) : (
<center className="mt-5 p-2">

View file

@ -0,0 +1,100 @@
import React, { useState, useEffect } from 'react';
import Select from '@/_ui/Select';
import defaultStyles from '@/_ui/Select/styles';
import { CodeHinter } from '../../CodeBuilder/CodeHinter';
import { useTranslation } from 'react-i18next';
export function SwitchPage({ getPages, currentState, event, handlerChanged, eventIndex, darkMode }) {
const queryParamChangeHandler = (index, key, value) => {
event.queryParams[index][key] = value;
handlerChanged(eventIndex, 'queryParams', event.queryParams);
};
const { t } = useTranslation();
const addQueryParam = () => {
if (!event.queryParams) {
event.queryParams = [];
handlerChanged(eventIndex, 'queryParams', event.queryParams);
}
event.queryParams.push(['', '']);
handlerChanged(eventIndex, 'queryParams', event.queryParams);
setNumberOfQueryparams(numberOfQueryParams + 1);
};
const deleteQueryParam = (index) => {
event.queryParams.splice(index, 1);
handlerChanged(eventIndex, 'queryParams', event.queryParams);
setNumberOfQueryparams(numberOfQueryParams - 1);
};
const [numberOfQueryParams, setNumberOfQueryparams] = useState(0);
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
if (event.queryParams) {
setNumberOfQueryparams(event.queryParams.length);
}
});
const styles = {
...defaultStyles(darkMode),
menuPortal: (provided) => ({ ...provided, zIndex: 9999 }),
menuList: (base) => ({
...base,
}),
};
return (
<div className="p-1 switch-page">
<label className="form-label mt-1">{t('globals.page', 'Page')}</label>
<Select
options={getPages()}
search={true}
value={event.pageId}
onChange={(value) => {
handlerChanged(eventIndex, 'pageId', value);
}}
placeholder={t('globals.select', 'Select') + '...'}
styles={styles}
useMenuPortal={false}
className={`${darkMode ? 'select-search-dark' : 'select-search'}`}
/>
<label className="form-label mt-2">Query params</label>
{Array(numberOfQueryParams)
.fill(0)
.map((_, index) => (
<div key={index} className="row input-group mt-1">
<div className="col">
<CodeHinter
currentState={currentState}
initialValue={event.queryParams[index][0]}
onChange={(value) => queryParamChangeHandler(index, 0, value)}
mode="javascript"
className="form-control codehinter-query-editor-input"
height={30}
/>
</div>
<div className="col">
<CodeHinter
currentState={currentState}
initialValue={event.queryParams[index][1]}
onChange={(value) => queryParamChangeHandler(index, 1, value)}
mode="javascript"
className="form-control codehinter-query-editor-input"
height={30}
/>
</div>
<span className="btn-sm col-auto my-1" role="button" onClick={() => deleteQueryParam(index)}>
x
</span>
</div>
))}
<button className="btn btn-sm btn-outline-azure mt-2 mx-0 mb-0" onClick={addQueryParam}>
+
</button>
</div>
);
}

View file

@ -14,7 +14,8 @@ export const DefaultComponent = ({ componentMeta, darkMode, ...restProps }) => {
currentState,
eventsChanged,
apps,
allComponents,
components,
pages,
} = restProps;
const properties = Object.keys(componentMeta.properties);
@ -32,9 +33,10 @@ export const DefaultComponent = ({ componentMeta, darkMode, ...restProps }) => {
currentState,
eventsChanged,
apps,
allComponents,
components,
validations,
darkMode
darkMode,
pages
);
return <Accordion items={accordionItems} />;
@ -53,7 +55,8 @@ export const baseComponentProperties = (
apps,
allComponents,
validations,
darkMode
darkMode,
pages
) => {
// Add widget title to section key to filter that property section from specified widgets' settings
const accordionFilters = {
@ -97,6 +100,7 @@ export const baseComponentProperties = (
eventsChanged={eventsChanged}
apps={apps}
darkMode={darkMode}
pages={pages}
/>
),
});

View file

@ -19,6 +19,7 @@ export function Icon({ componentMeta, darkMode, ...restProps }) {
eventsChanged,
apps,
allComponents,
pages,
} = restProps;
const [searchText, setSearchText] = useState('');
@ -143,6 +144,7 @@ export function Icon({ componentMeta, darkMode, ...restProps }) {
eventsChanged={eventsChanged}
apps={apps}
darkMode={darkMode}
pages={pages}
/>
),
});

View file

@ -417,6 +417,7 @@ class TableComponent extends React.Component {
popOverCallback={(showing) => {
this.setColumnPopoverRootCloseBlocker('event-manager', showing);
}}
pages={this.props.pages}
/>
</div>
)}
@ -753,6 +754,7 @@ class TableComponent extends React.Component {
this.setState({ actionPopOverRootClose: !showing });
this.setState({ showPopOver: showing });
}}
pages={this.props.pages}
/>
<button className="btn btn-sm btn-outline-danger mt-2 col" onClick={() => this.removeAction(index)}>
{this.props.t('widget.Table.remove', 'Remove')}
@ -1068,7 +1070,7 @@ class TableComponent extends React.Component {
items.push({
title: 'Events',
isOpen: false,
isOpen: true,
children: (
<EventManager
component={component}
@ -1078,6 +1080,7 @@ class TableComponent extends React.Component {
components={components}
eventsChanged={this.props.eventsChanged}
apps={this.props.apps}
pages={this.props.pages}
/>
),
});

View file

@ -4,6 +4,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Popover from 'react-bootstrap/Popover';
import { CodeHinter } from '../CodeBuilder/CodeHinter';
import { GotoApp } from './ActionConfigurationPanels/GotoApp';
import { SwitchPage } from './ActionConfigurationPanels/SwitchPage';
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
import useDraggableInPortal from '@/_hooks/useDraggableInPortal';
import _ from 'lodash';
@ -23,7 +24,9 @@ export const EventManager = ({
excludeEvents,
popOverCallback,
popoverPlacement,
pages,
}) => {
const [events, setEvents] = useState(() => component.component.definition.events || []);
const [focusedEventIndex, setFocusedEventIndex] = useState(null);
const { t } = useTranslation();
@ -160,20 +163,29 @@ export const EventManager = ({
return appsOptionsList;
}
function getPageOptions() {
return pages.map((page) => ({
name: page.name,
value: page.id,
}));
}
function handlerChanged(index, param, value) {
let newEvents = component.component.definition.events;
let newEvents = [...events];
let updatedEvent = newEvents[index];
updatedEvent[param] = value;
newEvents[index] = updatedEvent;
setEvents(newEvents);
eventsChanged(newEvents);
}
function removeHandler(index) {
let newEvents = component.component.definition.events;
newEvents.splice(index, 1);
setEvents(newEvents);
eventsChanged(newEvents);
}
@ -185,6 +197,7 @@ export const EventManager = ({
message: 'Hello world!',
alertType: 'info',
});
setEvents(newEvents);
eventsChanged(newEvents);
}
function eventPopover(event, index) {
@ -530,6 +543,60 @@ export const EventManager = ({
</div>
</>
)}
{event.actionId === 'set-page-variable' && (
<>
<div className="row">
<div className="col-3 p-2">{t('editor.inspector.eventManager.key', 'Key')}</div>
<div className="col-9">
<CodeHinter
theme={darkMode ? 'monokai' : 'default'}
currentState={currentState}
initialValue={event.key}
onChange={(value) => handlerChanged(index, 'key', value)}
enablePreview={true}
/>
</div>
</div>
<div className="row mt-3">
<div className="col-3 p-2">{t('editor.inspector.eventManager.value', 'Value')}</div>
<div className="col-9">
<CodeHinter
theme={darkMode ? 'monokai' : 'default'}
currentState={currentState}
initialValue={event.value}
onChange={(value) => handlerChanged(index, 'value', value)}
enablePreview={true}
/>
</div>
</div>
</>
)}
{event.actionId === 'unset-page-variable' && (
<>
<div className="row">
<div className="col-3 p-2">{t('editor.inspector.eventManager.key', 'Key')}</div>
<div className="col-9">
<CodeHinter
theme={darkMode ? 'monokai' : 'default'}
currentState={currentState}
initialValue={event.key}
onChange={(value) => handlerChanged(index, 'key', value)}
enablePreview={true}
/>
</div>
</div>
</>
)}
{event.actionId === 'switch-page' && (
<SwitchPage
event={event}
handlerChanged={handlerChanged}
eventIndex={index}
getPages={getPageOptions}
currentState={currentState}
darkMode={darkMode}
/>
)}
{event.actionId === 'control-component' && (
<>
<div className="row">
@ -624,6 +691,7 @@ export const EventManager = ({
const result = [...component.component.definition.events];
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
setEvents(result);
eventsChanged(result, true);
};
@ -669,6 +737,7 @@ export const EventManager = ({
setFocusedEventIndex(index);
} else {
setFocusedEventIndex(null);
eventsChanged(events);
}
if (typeof popOverCallback === 'function') popOverCallback(showing);
}}
@ -771,7 +840,6 @@ export const EventManager = ({
);
};
const events = component.component.definition.events || [];
const componentName = componentMeta.name ? componentMeta.name : 'query';
if (events.length === 0) {

View file

@ -18,6 +18,8 @@ import { Icon } from './Components/Icon';
import useFocus from '@/_hooks/use-focus';
import Accordion from '@/_ui/Accordion';
import { useTranslation } from 'react-i18next';
import _ from 'lodash';
import { useMounted } from '@/_hooks/use-mount';
export const Inspector = ({
selectedComponentId,
@ -31,6 +33,7 @@ export const Inspector = ({
removeComponent,
handleEditorEscapeKeyPress,
appDefinitionLocalVersion,
pages,
}) => {
const component = {
id: selectedComponentId,
@ -59,6 +62,8 @@ export const Inspector = ({
}
}, []);
const isMounted = useMounted();
useEffect(() => {
componentNameRef.current = newComponentName;
}, [newComponentName]);
@ -116,7 +121,9 @@ export const Inspector = ({
};
function paramUpdated(param, attr, value, paramType) {
let newDefinition = { ...component.component.definition };
console.log({ param, attr, value, paramType });
let newDefinition = _.cloneDeep(component.component.definition);
let allParams = newDefinition[paramType] || {};
const paramObject = allParams[param.name];
if (!paramObject) {
@ -135,13 +142,11 @@ export const Inspector = ({
newDefinition[paramType] = allParams;
let newComponent = {
...component,
let newComponent = _.merge(component, {
component: {
...component.component,
definition: newDefinition,
},
};
});
componentDefinitionChanged(newComponent);
}
@ -238,141 +243,6 @@ export const Inspector = ({
componentDefinitionChanged(newComponent);
}
function getAccordion(componentName) {
switch (componentName) {
case 'Table':
return (
<Table
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
eventUpdated={eventUpdated}
eventOptionUpdated={eventOptionUpdated}
components={allComponents}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
/>
);
case 'Chart':
return (
<Chart
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
eventUpdated={eventUpdated}
eventOptionUpdated={eventOptionUpdated}
components={allComponents}
currentState={currentState}
darkMode={darkMode}
/>
);
case 'FilePicker':
return (
<FilePicker
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
allComponents={allComponents}
/>
);
case 'Modal':
return (
<Modal
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
allComponents={allComponents}
/>
);
case 'CustomComponent':
return (
<CustomComponent
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
allComponents={allComponents}
/>
);
case 'Icon':
return (
<Icon
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
allComponents={allComponents}
/>
);
case 'Form':
return (
<Form
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
allComponents={allComponents}
/>
);
default: {
return (
<DefaultComponent
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
allComponents={allComponents}
/>
);
}
}
}
const buildGeneralStyle = () => {
const items = [];
@ -407,7 +277,7 @@ export const Inspector = ({
};
return (
<div className="inspector" key={appDefinitionLocalVersion}>
<div className="inspector">
<ConfirmDialog
show={showWidgetDeleteConfirmation}
message={'Widget will be deleted, do you want to continue?'}
@ -446,7 +316,24 @@ export const Inspector = ({
</div>
</div>
</div>
{getAccordion(componentMeta.component)}
{isMounted && (
<GetAccordion
componentName={componentMeta.component}
layoutPropertyChanged={layoutPropertyChanged}
component={component}
paramUpdated={paramUpdated}
dataQueries={dataQueries}
componentMeta={componentMeta}
eventUpdated={eventUpdated}
eventOptionUpdated={eventOptionUpdated}
components={allComponents}
currentState={currentState}
darkMode={darkMode}
eventsChanged={eventsChanged}
apps={apps}
pages={pages}
/>
)}
</Tab>
<Tab eventKey="styles" title={t('widget.common.styles', 'Styles')}>
<div style={{ marginBottom: '6rem' }}>
@ -575,4 +462,35 @@ const handleRenderingConditionalStyles = (
: null;
};
const GetAccordion = React.memo(
({ componentName, ...restProps }) => {
switch (componentName) {
case 'Table':
return <Table {...restProps} />;
case 'Chart':
return <Chart {...restProps} />;
case 'FilePicker':
return <FilePicker {...restProps} />;
case 'Modal':
return <Modal {...restProps} />;
case 'CustomComponent':
return <CustomComponent {...restProps} />;
case 'Icon':
return <Icon {...restProps} />;
default: {
return <DefaultComponent {...restProps} />;
}
}
},
(prevProps, nextProps) => {
prevProps.componentName === nextProps.componentName;
}
);
Inspector.RenderStyleOptions = RenderStyleOptions;

View file

@ -4,19 +4,19 @@ import { LeftSidebarItem } from './SidebarItem';
import { commentsService } from '@/_services';
import useRouter from '@/_hooks/use-router';
export const LeftSidebarComment = ({ toggleComments, appVersionsId }) => {
export const LeftSidebarComment = ({ toggleComments, appVersionsId, currentPageId }) => {
const [isActive, toggleActive] = React.useState(false);
const [notifications, setNotifications] = React.useState([]);
const router = useRouter();
React.useEffect(() => {
if (appVersionsId) {
commentsService.getNotifications(router.query.id, false, appVersionsId).then(({ data }) => {
commentsService.getNotifications(router.query.id, false, appVersionsId, currentPageId).then(({ data }) => {
setNotifications(data);
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [appVersionsId]);
}, [appVersionsId, currentPageId]);
return (
<LeftSidebarItem

View file

@ -7,10 +7,11 @@ import { SidebarPinnedButton } from './SidebarPinnedButton';
import { useTranslation } from 'react-i18next';
import JSONTreeViewer from '@/_ui/JSONTreeViewer';
export const LeftSidebarDebugger = ({ darkMode, errors, debuggerActions }) => {
export const LeftSidebarDebugger = ({ darkMode, errors, debuggerActions, currentPageId }) => {
const { t } = useTranslation();
const [open, trigger, content, popoverPinned, updatePopoverPinnedState] = usePinnedPopover(false);
const [errorLogs, setErrorLogs] = React.useState([]);
const [errorHistory, setErrorHistory] = React.useState({ appLevel: [], pageLevel: [] });
const [unReadErrorCount, setUnReadErrorCount] = React.useState({ read: 0, unread: 0 });
const clearErrorLogs = () => {
@ -19,8 +20,18 @@ export const LeftSidebarDebugger = ({ darkMode, errors, debuggerActions }) => {
});
setErrorLogs(() => []);
setErrorHistory(() => ({ appLevel: [], pageLevel: [] }));
};
React.useEffect(() => {
if (currentPageId) {
const olderPageErrorFromHistory = errorHistory.pageLevel[currentPageId] ?? [];
const olderAppErrorFromHistory = errorHistory.appLevel ?? [];
setErrorLogs(() => [...olderPageErrorFromHistory, ...olderAppErrorFromHistory]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPageId]);
React.useEffect(() => {
const newError = _.flow([
Object.entries,
@ -30,17 +41,30 @@ export const LeftSidebarDebugger = ({ darkMode, errors, debuggerActions }) => {
])(errors);
const newErrorLogs = debuggerActions.generateErrorLogs(newError);
const newPageLevelErrorLogs = newErrorLogs.filter((error) => error.strace === 'page_level');
const newAppLevelErrorLogs = newErrorLogs.filter((error) => error.strace === 'app_level');
if (newErrorLogs) {
setErrorLogs((prevErrors) => {
const copy = JSON.parse(JSON.stringify(prevErrors));
return [...newErrorLogs, ...copy];
return [...newAppLevelErrorLogs, ...newPageLevelErrorLogs, ...copy];
});
setErrorHistory((prevErrors) => {
const copy = JSON.parse(JSON.stringify(prevErrors));
return {
appLevel: [...newAppLevelErrorLogs, ...copy.appLevel],
pageLevel: {
[currentPageId]: [...newPageLevelErrorLogs, ...(copy.pageLevel[currentPageId] ?? [])],
},
};
});
}
debuggerActions.flush();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(errors)]);
}, [JSON.stringify({ errors })]);
React.useEffect(() => {
const unReadErrors = open ? 0 : errorLogs.length - unReadErrorCount.read;

View file

@ -8,6 +8,7 @@ import FxButton from '../CodeBuilder/Elements/FxButton';
import { CodeHinter } from '../CodeBuilder/CodeHinter';
import { resolveReferences } from '@/_helpers/utils';
import { useTranslation } from 'react-i18next';
import _ from 'lodash';
export const LeftSidebarGlobalSettings = ({
globalSettings,
@ -19,7 +20,8 @@ export const LeftSidebarGlobalSettings = ({
}) => {
const { t } = useTranslation();
const [open, trigger, content] = usePopover(false);
const { hideHeader, canvasMaxWidth, canvasMaxHeight, canvasBackgroundColor, backgroundFxQuery } = globalSettings;
const { hideHeader, canvasMaxWidth, canvasMaxWidthType, canvasMaxHeight, canvasBackgroundColor, backgroundFxQuery } =
globalSettings;
const [showPicker, setShowPicker] = React.useState(false);
const [forceCodeBox, setForceCodeBox] = React.useState(true);
const [realState, setRealState] = React.useState(currentState);
@ -103,7 +105,26 @@ export const LeftSidebarGlobalSettings = ({
}}
value={canvasMaxWidth}
/>
<span className="input-group-text">px</span>
<select
className="form-select"
aria-label="Select canvas width type"
onChange={(event) => {
const newCanvasMaxWidthType = event.currentTarget.value;
globalSettingsChanged('canvasMaxWidthType', newCanvasMaxWidthType);
if (newCanvasMaxWidthType === '%') {
globalSettingsChanged('canvasMaxWidth', 100);
} else if (newCanvasMaxWidthType === 'px') {
globalSettingsChanged('canvasMaxWidth', 1292);
}
}}
>
<option value="%" selected={canvasMaxWidthType === '%'}>
%
</option>
<option value="px" selected={canvasMaxWidthType === 'px' || _.isUndefined(canvasMaxWidthType)}>
px
</option>
</select>
</div>
</div>
</div>
@ -116,7 +137,7 @@ export const LeftSidebarGlobalSettings = ({
<input
data-cy="maximum-canvas-height-input-field"
type="text"
className={`form-control form-control-sm`}
className={`form-control form-control-sm maximum-canvas-height-input-field`}
placeholder={'0'}
onChange={(e) => {
const height = e.target.value;
@ -142,7 +163,7 @@ export const LeftSidebarGlobalSettings = ({
color={canvasBackgroundColor}
onChangeComplete={(color) => {
globalSettingsChanged('canvasBackgroundColor', [color.hex, color.rgb]);
globalSettingsChanged('backgroundFxQuery', null);
globalSettingsChanged('backgroundFxQuery', color.hex);
}}
/>
</div>

View file

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import { ToolTip } from '@/Editor/Inspector/Elements/Components/ToolTip';
export const EditInput = ({ slug, error, setError, pageHandle, setPageHandle, isSaving = false }) => {
const [value, set] = useState(pageHandle);
const onChangePageHandleValue = (event) => {
setError(null);
const newHandle = event.target.value;
if (newHandle === '') setError('Page handle cannot be empty');
if (newHandle === value) setError('Page handle cannot be same as the existing page handle');
set(newHandle);
};
React.useEffect(() => {
if (!isSaving) {
setPageHandle(value);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
React.useEffect(() => {
if (isSaving) {
set(pageHandle);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSaving]);
const label = <span style={{ color: '#889096' }}>{slug.substring(0, 16)}.../</span>;
return (
<div className="input-group col">
<div className="input-group-text">
<ToolTip label={label} meta={{ tip: slug }} labelClass="page-handle-tip" />
</div>
<input
type="text"
className={`page-handler-input form-control form-control-sm ${error ? 'is-invalid' : ''}`}
placeholder={'Enter page handle'}
onChange={onChangePageHandleValue}
value={pageHandle}
/>
<div className="invalid-feedback">{error}</div>
</div>
);
};

View file

@ -0,0 +1,107 @@
import React, { useState } from 'react';
import { Modal } from 'react-bootstrap';
import { Button } from '@/_ui/LeftSidebar';
import { Alert } from '@/_ui/Alert';
import { EditInput } from './EditInput';
export const EditModal = ({ slug, page, show, handleClose, updatePageHandle, darkMode }) => {
const [pageHandle, setPageHandle] = useState(page.handle);
const [error, setError] = useState(null);
const [isSaving, setIsSaving] = useState(false);
React.useEffect(() => {
setError(null);
}, [show]);
const handleSave = () => {
if (pageHandle === page.handle) {
return handleClose();
}
setIsSaving(true);
updatePageHandle(page.id, pageHandle);
setTimeout(() => {
setIsSaving(false);
return handleClose();
}, 900);
};
const handleCancel = () => {
setError(null);
setIsSaving(false);
handleClose();
};
React.useEffect(() => {
if (!show && pageHandle !== page.handle) {
setPageHandle(page.handle);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [show]);
return (
<Modal
show={show}
onHide={handleClose}
size="sm"
centered
className={`${darkMode && 'theme-dark'} page-handle-edit-modal `}
backdrop="static"
onClick={(event) => event.stopPropagation()}
>
<Modal.Header>
<Modal.Title style={{ fontSize: '16px', fontWeight: '400' }}>Edit page handle</Modal.Title>
<span className="cursor-pointer" size="sm" onClick={() => handleClose()}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icon-tabler-x"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</span>
</Modal.Header>
<Modal.Body>
<div className="page-handle-edit-container mb-4">
<EditInput
slug={slug}
error={error}
setError={setError}
pageHandle={pageHandle}
setPageHandle={setPageHandle}
isSaving={isSaving}
/>
</div>
<div className="alert-container">
<Alert svg="alert-info" cls="page-handler-alert">
Changing the page handle will break any existing apps that are using this page.
</Alert>
</div>
</Modal.Body>
<Modal.Footer>
<Button darkMode={darkMode} onClick={handleCancel} styles={{ height: '32px' }} disabled={isSaving}>
<Button.Content title="Cancel" />
</Button>
<Button
darkMode={darkMode}
onClick={handleSave}
styles={{ backgroundColor: '#3E63DD', color: '#FDFDFE', height: '32px' }}
disabled={error !== null || pageHandle === page.handle}
isLoading={isSaving}
>
<Button.Content title="Save" iconSrc="assets/images/icons/save.svg" direction="left" />
</Button>
</Modal.Footer>
</Modal>
);
};

View file

@ -0,0 +1,65 @@
import React from 'react';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import { Button } from '@/_ui/LeftSidebar';
export const GlobalSettings = ({
darkMode,
handlePopoverPinnedState,
showHideViewerNavigationControls,
showPageViwerPageNavitation,
}) => {
const onChange = () => {
showHideViewerNavigationControls();
};
return (
<OverlayTrigger
trigger={'click'}
placement={'bottom-end'}
rootClose={true}
onToggle={handlePopoverPinnedState}
overlay={
<Popover id="page-handler-menu" className={`global-settings ${darkMode && 'popover-dark-themed'}`}>
<Popover.Content bsPrefix="popover-body">
<div className="card-body">
<label htmlFor="pin" className="form-label">
Settings
</label>
<hr style={{ margin: '0.75rem 0' }} />
<div className="menu-options mb-0">
<Toggle onChange={onChange} value={!showPageViwerPageNavitation} />
</div>
</div>
</Popover.Content>
</Popover>
}
>
<Button darkMode={darkMode} onClick={null} size="sm" styles={{ width: '28px', padding: 0 }}>
<Button.Content iconSrc="assets/images/icons/editor/left-sidebar/settings.svg" />
</Button>
</OverlayTrigger>
);
};
const Toggle = ({ onChange, value = true }) => {
return (
<div className="form-check form-switch">
<input
className="form-check-input"
type="checkbox"
onClick={(e) => {
e.stopPropagation();
onChange();
}}
checked={value}
/>
<span className="form-check-label">Disable Menu</span>
<div className="toggle-info">
<small className="secondary-text">
To hide the page navigation sidebar in viwer mode, set this option to on.
</small>
</div>
</div>
);
};

View file

@ -0,0 +1,226 @@
import React, { useState } from 'react';
import { RenameInput } from './RenameInput';
import { PagehandlerMenu } from './PagehandlerMenu';
import { EditModal } from './EditModal';
import { SettingsModal } from './SettingsModal';
import _ from 'lodash';
import SortableList from '@/_components/SortableList';
export const PageHandler = ({
darkMode,
page,
switchPage,
deletePage,
renamePage,
clonePage,
hidePage,
unHidePage,
updatePopoverPinnedState,
homePageId,
currentPageId,
updateHomePage,
updatePageHandle,
updateOnPageLoadEvents,
currentState,
apps,
allPages,
components,
dataQueries,
}) => {
const isHomePage = page.id === homePageId;
const isSelected = page.id === currentPageId;
const isHidden = page?.hidden ?? false;
const [isEditingPageName, setIsEditingPageName] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showPagehandlerMenu, setShowPagehandlerMenu] = useState(false);
const [showSettingsModal, setShowSettingsModal] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const handleClose = () => {
setShowEditModal(false);
setShowPagehandlerMenu(true);
};
const handleShow = () => {
setShowEditModal(true);
setShowPagehandlerMenu(false);
};
const showSettings = () => {
setShowSettingsModal(true);
};
React.useEffect(() => {
if (showPagehandlerMenu) {
updatePopoverPinnedState(true);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showPagehandlerMenu]);
const handleCallback = (id) => {
setIsHovered(false);
switch (id) {
case 'delete-page':
deletePage(page.id, isHomePage);
break;
case 'rename-page':
setIsEditingPageName(true);
break;
case 'mark-as-home-page':
updateHomePage(page.id);
break;
case 'edit-page-handle':
handleShow();
break;
case 'settings':
showSettings();
break;
case 'duplicate-page':
clonePage(page.id);
break;
case 'hide-page':
hidePage(page.id);
break;
case 'unhide-page':
unHidePage(page.id);
break;
default:
break;
}
};
React.useEffect(() => {
if (!isHovered && !isSelected && showPagehandlerMenu) {
setShowPagehandlerMenu(false);
}
if (isHovered && currentPageId !== page.id) {
setShowPagehandlerMenu(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isHovered]);
if (isEditingPageName) {
return <RenameInput page={page} updaterCallback={renamePage} updatePageEditMode={setIsEditingPageName} />;
}
const windowUrl = window.location.href;
const slug = windowUrl.split(page.handle)[0];
return (
<div
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
className={`card cursor-pointer ${isSelected ? 'active' : 'non-active-page'}`}
onClick={() => page.id != currentPageId && switchPage(page.id)}
>
<div className="card-body">
<div className="row" role="button">
<div className="col-auto">
<SortableList.DragHandle show={isHovered} />
</div>
<div className="col text-truncate" data-cy="event-handler">
{page.name}
</div>
<div className="col-auto page-icons">
{isHidden && (
<img
data-toggle="tooltip"
title="hidden"
className="mx-2"
src="assets/images/icons/eye-off.svg"
height={14}
width={14}
/>
)}
{(isHovered || isSelected) && isHomePage && (
<img
data-toggle="tooltip"
title="home page"
className="mx-2"
src="assets/images/icons/home.svg"
height={14}
width={14}
/>
)}
</div>
<div className="col-auto">
{(isHovered || isSelected) && (
<PagehandlerMenu
page={page}
darkMode={darkMode}
handlePageCallback={handleCallback}
showMenu={showPagehandlerMenu}
setShowMenu={setShowPagehandlerMenu}
isHome={isHomePage}
isHidden={isHidden}
/>
)}
<EditModal
slug={slug}
page={page}
show={showEditModal}
handleClose={handleClose}
updatePageHandle={updatePageHandle}
darkMode={darkMode}
/>
<SettingsModal
page={page}
show={showSettingsModal}
handleClose={() => setShowSettingsModal(false)}
darkMode={darkMode}
updateOnPageLoadEvents={updateOnPageLoadEvents}
currentState={currentState}
apps={apps}
pages={allPages}
components={components}
dataQueries={dataQueries}
/>
</div>
</div>
</div>
</div>
);
};
export const AddingPageHandler = ({ addNewPage, setNewPageBeingCreated }) => {
const handleAddingNewPage = (pageName) => {
if (pageName) {
addNewPage({ name: pageName, handle: _.kebabCase(pageName.toLowerCase()) });
}
setNewPageBeingCreated(false);
};
return (
<div className="row" role="button">
<div className="col-12">
<input
type="text"
className="form-control page-name-input"
autoFocus
onBlur={(event) => {
const name = event.target.value;
handleAddingNewPage(name);
event.stopPropagation();
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
const name = event.target.value;
handleAddingNewPage(name);
event.stopPropagation();
}
}}
/>
</div>
</div>
);
};

View file

@ -0,0 +1,152 @@
import React from 'react';
import { OverlayTrigger, Popover } from 'react-bootstrap';
import { Button } from '@/_ui/LeftSidebar';
export const PagehandlerMenu = ({ page, darkMode, handlePageCallback, showMenu, setShowMenu, isHome, isHidden }) => {
const closeMenu = () => {
setShowMenu(false);
};
React.useEffect(() => {
const handleClickOutside = (event) => {
if (showMenu && event.target.closest('.pagehandler-menu') === null) {
closeMenu();
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ page, showMenu })]);
return (
<OverlayTrigger
trigger={'click'}
placement={'bottom-end'}
rootClose={false}
show={showMenu}
overlay={
<Popover key={page.id} id="page-handler-menu" className={darkMode && 'popover-dark-themed'}>
<Popover.Content key={page.id} bsPrefix="popover-body">
<div className="card-body">
<PageHandleField page={page} updatePageHandle={handlePageCallback} />
<hr style={{ margin: '0.75rem 0' }} />
<div className="menu-options mb-0">
<Field
id="rename-page"
text="Rename"
iconSrc={'assets/images/icons/input.svg'}
closeMenu={closeMenu}
callback={handlePageCallback}
/>
<Field
id="duplicate-page"
text="Duplicate"
iconSrc={'assets/images/icons/duplicate.svg'}
closeMenu={closeMenu}
callback={handlePageCallback}
/>
<Field
id="mark-as-home-page"
text="Mark home"
iconSrc={'assets/images/icons/home.svg'}
closeMenu={closeMenu}
callback={handlePageCallback}
/>
<Field
id={isHidden ? 'unhide-page' : 'hide-page'}
text={isHidden ? 'Unhide page' : 'Hide page'}
iconSrc={`assets/images/icons/${isHidden ? 'eye' : 'eye-off'}.svg`}
closeMenu={closeMenu}
callback={handlePageCallback}
/>
<Field
id="delete-page"
text="Delete page"
iconSrc={'assets/images/icons/delete.svg'}
customClass={isHome ? 'delete-btn' : 'field__danger delete-btn'}
closeMenu={closeMenu}
callback={handlePageCallback}
disabled={isHome}
/>
<Field
id="settings"
text="Event Handlers"
customClass={'delete-btn'}
iconSrc={'assets/images/icons/editor/left-sidebar/page-settings.svg'}
closeMenu={closeMenu}
callback={handlePageCallback}
/>
</div>
</div>
</Popover.Content>
</Popover>
}
>
<Button.UnstyledButton
onClick={(event) => {
event.stopPropagation();
setShowMenu(true);
}}
styles={{ height: '20px' }}
>
<Button.Content iconSrc={'assets/images/icons/3dots-menu.svg'} />
</Button.UnstyledButton>
</OverlayTrigger>
);
};
const PageHandleField = ({ page, updatePageHandle }) => {
const Label = () => {
return (
<label htmlFor="pin" className="form-label">
Page Handle
</label>
);
};
const content = () => {
return (
<div className="col">
<span style={{ color: '#889096' }}>.../</span>
<span>{page.handle}</span>
</div>
);
};
return (
<div className="mb-2 px-2">
<Label />
<Button.UnstyledButton
onClick={(e) => {
e.stopPropagation();
updatePageHandle('edit-page-handle');
}}
classNames="page-handle-button-container"
>
<Button.Content title={content} iconSrc={'assets/images/icons/input.svg'} direction="right" />
</Button.UnstyledButton>
</div>
);
};
const Field = ({ id, text, iconSrc, customClass = '', closeMenu, disabled = false, callback = () => null }) => {
const handleOnClick = (e) => {
e.stopPropagation();
closeMenu();
callback(id);
};
return (
<div className={`field ${customClass ? ` ${customClass}` : ''}`}>
<Button.UnstyledButton onClick={handleOnClick} styles={{ height: '28px' }} disabled={disabled}>
<Button.Content title={text} iconSrc={iconSrc} direction="left" />
</Button.UnstyledButton>
</div>
);
};

View file

@ -0,0 +1,35 @@
import React from 'react';
export const RenameInput = ({ page, updaterCallback, updatePageEditMode }) => {
const handleAddingNewPage = (pageName) => {
if (pageName.length > 0 && pageName !== page.name) {
updaterCallback(page.id, pageName);
}
updatePageEditMode(false);
};
return (
<div className="row" role="button">
<div className="col-12">
<input
type="text"
className="form-control page-name-input"
autoFocus
defaultValue={page.name}
onBlur={(event) => {
const name = event.target.value;
handleAddingNewPage(name);
event.stopPropagation();
}}
onKeyDown={(event) => {
if (event.key === 'Enter') {
const name = event.target.value;
handleAddingNewPage(name);
event.stopPropagation();
}
}}
/>
</div>
</div>
);
};

View file

@ -0,0 +1,82 @@
import React, { useState } from 'react';
import { Modal } from 'react-bootstrap';
import { Button } from '@/_ui/LeftSidebar';
import { EventManager } from '../../Inspector/EventManager';
export const SettingsModal = ({
page,
show,
handleClose,
darkMode,
updateOnPageLoadEvents,
currentState,
apps,
pages,
components,
dataQueries,
}) => {
const [isSaving, _setIsSaving] = useState(false);
console.log({ dataQueries });
return (
<div onClick={(event) => event.stopPropagation()}>
<Modal
show={show}
onHide={handleClose}
size="sm"
centered
className={`${darkMode && 'theme-dark'} page-handle-edit-modal`}
backdrop="static"
enforceFocus={false}
onClick={(event) => event.stopPropagation()}
>
<Modal.Header>
<Modal.Title style={{ fontSize: '16px', fontWeight: '400' }}>Page Events</Modal.Title>
<span className="cursor-pointer" size="sm" onClick={handleClose}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icon-tabler-x"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</span>
</Modal.Header>
<Modal.Body>
<b>Events</b>
<EventManager
component={{
component: {
definition: {
events: page.events ?? [],
},
},
}}
componentMeta={{ events: { onPageLoad: { displayName: 'On page load' } } }}
currentState={currentState}
dataQueries={dataQueries}
components={components}
apps={apps}
pages={pages}
eventsChanged={(events) => updateOnPageLoadEvents(page.id, events)}
popOverCallback={(showing) => showing}
/>
</Modal.Body>
<Modal.Footer>
<Button darkMode={darkMode} styles={{ height: '32px' }} disabled={isSaving} onClick={handleClose}>
<Button.Content title="Close" />
</Button>
</Modal.Footer>
</Modal>
</div>
);
};

View file

@ -0,0 +1,175 @@
import React, { useState } from 'react';
import Fuse from 'fuse.js';
import { LeftSidebarItem } from '../SidebarItem';
import usePinnedPopover from '@/_hooks/usePinnedPopover';
import { Button, HeaderSection } from '@/_ui/LeftSidebar';
import { SidebarPinnedButton } from '../SidebarPinnedButton';
import { PageHandler, AddingPageHandler } from './PageHandler';
import { GlobalSettings } from './GlobalSettings';
import _ from 'lodash';
import SortableList from '@/_components/SortableList';
const LeftSidebarPageSelector = ({
appDefinition,
darkMode,
currentPageId,
addNewPage,
switchPage,
deletePage,
renamePage,
clonePage,
hidePage,
unHidePage,
updateHomePage,
updatePageHandle,
pages,
homePageId,
showHideViewerNavigationControls,
updateOnSortingPages,
updateOnPageLoadEvents,
currentState,
apps,
dataQueries,
}) => {
const [open, trigger, content, popoverPinned, updatePopoverPinnedState] = usePinnedPopover(false);
const handlePopoverPinnedState = () => {
if (!popoverPinned) {
updatePopoverPinnedState(true);
}
};
const [allpages, setPages] = useState(pages);
const [newPageBeingCreated, setNewPageBeingCreated] = useState(false);
const filterPages = (value) => {
if (!value || value.length === 0) return clearSearch();
const fuse = new Fuse(allpages, { keys: ['name'], threshold: 0.3 });
const result = fuse.search(value);
setPages(result.map((item) => item.item));
};
const clearSearch = () => {
setPages(pages);
};
React.useEffect(() => {
if (!_.isEqual(pages, allpages)) {
setPages(pages);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify({ pages })]);
return (
<>
<LeftSidebarItem
tip="Pages"
{...trigger}
icon="page"
className={`left-sidebar-item left-sidebar-layout ${open && 'active'} left-sidebar-page-selector`}
text={'Pages'}
/>
<div
{...content}
className={`card popover left-sidebar-page-selector ${open || popoverPinned ? 'show' : 'hide'} ${
darkMode && 'dark'
} `}
style={{
minWidth: '295px',
top: '45px',
borderRadius: '0px',
height: '100%',
maxHeight: window.innerHeight,
overflowX: 'hidden',
}}
>
<div className="card-body p-0" onClick={(event) => event.stopPropagation()}>
<HeaderSection darkMode={darkMode}>
<HeaderSection.PanelHeader title="Pages">
<div className="d-flex justify-content-end">
<Button
onClick={() => setNewPageBeingCreated(true)}
darkMode={darkMode}
size="sm"
styles={{ width: '76px' }}
>
<Button.Content title={'Add'} iconSrc={'assets/images/icons/plus.svg'} direction="left" />
</Button>
<GlobalSettings
darkMode={darkMode}
handlePopoverPinnedState={handlePopoverPinnedState}
showHideViewerNavigationControls={showHideViewerNavigationControls}
showPageViwerPageNavitation={appDefinition?.showViewerNavigation || false}
/>
<SidebarPinnedButton
darkMode={darkMode}
component={'PageSelector'}
state={popoverPinned}
updateState={updatePopoverPinnedState}
/>
</div>
</HeaderSection.PanelHeader>
<HeaderSection.SearchBoxComponent onChange={filterPages} placeholder={'Search'} placeholderIcon={'⌘S'} />
</HeaderSection>
<div className={`${darkMode && 'dark'} page-selector-panel-body`}>
<div className="">
{allpages.length > 0 ? (
<SortableList
data={allpages}
Element={PageHandler}
pages={allpages}
darkMode={darkMode}
switchPage={switchPage}
deletePage={deletePage}
renamePage={renamePage}
clonePage={clonePage}
hidePage={hidePage}
unHidePage={unHidePage}
updatePopoverPinnedState={handlePopoverPinnedState}
homePageId={homePageId}
currentPageId={currentPageId}
updateHomePage={updateHomePage}
updatePageHandle={updatePageHandle}
classNames="page-handler"
onSort={updateOnSortingPages}
updateOnPageLoadEvents={updateOnPageLoadEvents}
currentState={currentState}
apps={apps}
allpages={pages}
components={appDefinition?.components ?? {}}
dataQueries={dataQueries}
/>
) : (
<div className="d-flex justify-content-center align-items-center" style={{ height: '100%' }}>
<div className="text-center">
<img src="assets/images/no-results.svg" alt="empty-page" />
<p className="mt-3">No pages found</p>
</div>
</div>
)}
{newPageBeingCreated && (
<div className="page-handler">
<AddingPageHandler
addNewPage={addNewPage}
setNewPageBeingCreated={setNewPageBeingCreated}
switchPage={switchPage}
/>
</div>
)}
</div>
</div>
</div>
</div>
</>
);
};
export default LeftSidebarPageSelector;

View file

@ -1,26 +1,36 @@
import React from 'react';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';
import { Button } from '@/_ui/LeftSidebar';
export const SidebarPinnedButton = ({ state, component, updateState, darkMode }) => {
const tooltipMsg = state ? `Unpin ${component}` : `Pin ${component}`;
const pinnedIcon = !state ? 'pinned' : 'pinnedoff';
const iconSrc = `assets/images/icons/editor/left-sidebar/${pinnedIcon}.svg`;
// Todo: Uniform styles for all pinned buttons
return (
<SidebarPinnedButton.OverlayContainer tip={tooltipMsg}>
<div
className={`btn btn-sm btn-light m-1 ${darkMode && 'btn-outline-secondary'} ${state && 'active'} ${
component === 'Inspector' && 'position-absolute end-0'
}`}
onClick={updateState}
>
<img
className="svg-icon"
src={`assets/images/icons/editor/left-sidebar/${pinnedIcon}.svg`}
width="16"
height="16"
/>
</div>
{component === 'PageSelector' ? (
<Button darkMode={darkMode} onClick={updateState} size="sm" styles={{ width: '28px', padding: 0 }}>
<Button.Content iconSrc={iconSrc} />
</Button>
) : (
<div
className={`btn btn-sm btn-light m-1 ${darkMode && 'btn-outline-secondary'} ${state && 'active'} ${
component === 'Inspector' && 'position-absolute end-0'
}`}
onClick={updateState}
>
<img
className="svg-icon"
src={`assets/images/icons/editor/left-sidebar/${pinnedIcon}.svg`}
width="16"
height="16"
/>
</div>
)}
</SidebarPinnedButton.OverlayContainer>
);
};

View file

@ -9,6 +9,7 @@ import useRouter from '../../_hooks/use-router';
import { LeftSidebarDebugger } from './SidebarDebugger';
import { LeftSidebarComment } from './SidebarComment';
import { LeftSidebarGlobalSettings } from './SidebarGlobalSettings';
import LeftSidebarPageSelector from './SidebarPageSelector';
import { ConfirmDialog } from '@/_components';
import config from 'config';
@ -37,6 +38,21 @@ export const LeftSidebar = forwardRef((props, ref) => {
is_maintenance_on,
isSaving,
isUnsavedQueriesAvailable,
currentPageId,
addNewPage,
switchPage,
deletePage,
renamePage,
hidePage,
unHidePage,
updateHomePage,
updatePageHandle,
showHideViewerNavigationControls,
updateOnSortingPages,
updateOnPageLoadEvents,
apps,
dataQueries,
clonePage,
} = props;
const [showLeaveDialog, setShowLeaveDialog] = useState(false);
const [showDataSourceManagerModal, toggleDataSourceManagerModal] = useState(false);
@ -57,6 +73,28 @@ export const LeftSidebar = forwardRef((props, ref) => {
runQuery={runQuery}
dataSources={dataSources}
/>
<LeftSidebarPageSelector
darkMode={darkMode}
appDefinition={appDefinition}
currentPageId={currentPageId}
addNewPage={addNewPage}
switchPage={switchPage}
deletePage={deletePage}
renamePage={renamePage}
hidePage={hidePage}
unHidePage={unHidePage}
updateHomePage={updateHomePage}
updatePageHandle={updatePageHandle}
clonePage={clonePage}
pages={Object.entries(appDefinition.pages).map(([id, page]) => ({ id, ...page })) || []}
homePageId={appDefinition.homePageId}
showHideViewerNavigationControls={showHideViewerNavigationControls}
updateOnSortingPages={updateOnSortingPages}
updateOnPageLoadEvents={updateOnPageLoadEvents}
currentState={currentState}
apps={apps}
dataQueries={dataQueries}
/>
<LeftSidebarDataSources
darkMode={darkMode}
appId={appId}
@ -72,9 +110,14 @@ export const LeftSidebar = forwardRef((props, ref) => {
components={components}
errors={errorLogs}
debuggerActions={debuggerActions}
currentPageId={currentPageId}
/>
{config.COMMENT_FEATURE_ENABLE && (
<LeftSidebarComment appVersionsId={appVersionsId} toggleComments={toggleComments} />
<LeftSidebarComment
appVersionsId={appVersionsId}
toggleComments={toggleComments}
currentPageId={currentPageId}
/>
)}
<LeftSidebarGlobalSettings
currentState={currentState}

View file

@ -7,8 +7,10 @@ const MAX_DISPLAY_USERS = 3;
const RealtimeAvatars = () => {
const self = useSelf();
const others = useOthers();
const othersOnSameVersion = others.filter(
(other) => other?.presence?.editingVersionId === self?.presence.editingVersionId
const othersOnSameVersionAndPage = others.filter(
(other) =>
other?.presence?.editingVersionId === self?.presence.editingVersionId &&
other?.presence?.editingPageId === self?.presence.editingPageId
);
const getAvatarText = (presence) => presence.firstName?.charAt(0) + presence.lastName?.charAt(0);
@ -19,7 +21,7 @@ const RealtimeAvatars = () => {
// ref: https://github.com/wwayne/react-tooltip#3-tooltip-not-binding-to-dynamic-content
React.useEffect(() => {
ReactTooltip.rebuild();
}, [othersOnSameVersion?.length]);
}, [othersOnSameVersionAndPage?.length]);
return (
<div className="row realtime-avatars">
@ -35,7 +37,7 @@ const RealtimeAvatars = () => {
borderShape="rounded"
/>
)}
{othersOnSameVersion.slice(0, MAX_DISPLAY_USERS).map(({ id, presence }) => {
{othersOnSameVersionAndPage.slice(0, MAX_DISPLAY_USERS).map(({ id, presence }) => {
return (
<Avatar
key={id}
@ -47,8 +49,8 @@ const RealtimeAvatars = () => {
/>
);
})}
{othersOnSameVersion.length > MAX_DISPLAY_USERS && (
<Avatar text={`+${othersOnSameVersion.length - MAX_DISPLAY_USERS}`} borderShape="rounded" />
{othersOnSameVersionAndPage.length > MAX_DISPLAY_USERS && (
<Avatar text={`+${othersOnSameVersionAndPage.length - MAX_DISPLAY_USERS}`} borderShape="rounded" />
)}
</div>
</div>

View file

@ -7,7 +7,7 @@ import { Cursor } from './Cursor';
import { USER_COLORS } from '@/_helpers/constants';
import { userService } from '@/_services';
const RealtimeCursors = ({ editingVersionId }) => {
const RealtimeCursors = ({ editingVersionId, editingPageId }) => {
const currentUser = JSON.parse(localStorage.getItem('currentUser'));
const others = useOthers();
@ -23,9 +23,9 @@ const RealtimeCursors = ({ editingVersionId }) => {
}, []);
React.useEffect(() => {
updatePresence({ editingVersionId });
updatePresence({ editingVersionId, editingPageId });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [editingVersionId]);
}, [editingVersionId, editingPageId]);
React.useEffect(() => {
async function fetchAvatar() {
@ -40,8 +40,10 @@ const RealtimeCursors = ({ editingVersionId }) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentUser.avatar_id]);
const othersOnSameVersion = others.filter(
(other) => other?.presence?.editingVersionId === self?.presence.editingVersionId
const othersOnSameVersionAndPage = others.filter(
(other) =>
other?.presence?.editingVersionId === self?.presence.editingVersionId &&
other?.presence?.editingPageId === self?.presence.editingPageId
);
const handlePointerMove = React.useCallback(
@ -67,7 +69,7 @@ const RealtimeCursors = ({ editingVersionId }) => {
return (
<>
{othersOnSameVersion?.map(({ id, presence }) => {
{othersOnSameVersionAndPage?.map(({ id, presence }) => {
if (!presence) return null;
return <Cursor key={id} name={presence.firstName} color={presence.color} x={presence.x} y={presence.y} />;
})}

View file

@ -44,6 +44,7 @@ export const SubContainer = ({
exposedVariables,
addDefaultChildren = false,
height = '100%',
currentPageId,
}) => {
//Todo add custom resolve vars for other widgets too
const mounted = useMounted();
@ -65,7 +66,7 @@ export const SubContainer = ({
zoomLevel = zoomLevel || 1;
// eslint-disable-next-line react-hooks/exhaustive-deps
const allComponents = appDefinition ? appDefinition.components : {};
const allComponents = appDefinition ? appDefinition.pages[currentPageId].components : {};
const isParentModal =
(allComponents[parent]?.component?.component === 'Modal' ||
allComponents[parent]?.component?.component === 'Form') ??
@ -198,7 +199,17 @@ export const SubContainer = ({
useEffect(() => {
if (appDefinitionChanged) {
appDefinitionChanged({ ...appDefinition, components: boxes });
const newDefinition = {
...appDefinition,
pages: {
...appDefinition.pages,
[currentPageId]: {
...appDefinition.pages[currentPageId],
components: boxes,
},
},
};
appDefinitionChanged(newDefinition);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [boxes]);
@ -388,8 +399,8 @@ export const SubContainer = ({
function paramUpdated(id, param, value) {
if (Object.keys(value).length > 0) {
setBoxes(
update(boxes, {
setBoxes((boxes) => {
return update(boxes, {
[id]: {
$merge: {
component: {
@ -404,8 +415,8 @@ export const SubContainer = ({
},
},
},
})
);
});
});
}
}
@ -497,6 +508,7 @@ export const SubContainer = ({
hoveredComponent,
sideBarDebugger,
addDefaultChildren,
currentPageId,
}}
/>
);

View file

@ -4,6 +4,7 @@ import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import { Container } from './Container';
import { Confirm } from './Viewer/Confirm';
import { ViewerNavigation } from './Viewer/ViewerNavigation';
import {
onComponentOptionChanged,
onComponentOptionsChanged,
@ -20,6 +21,7 @@ import ViewerLogoIcon from './Icons/viewer-logo.svg';
import { DataSourceTypes } from './DataSourceManager/SourceComponents';
import { resolveReferences, safelyParseJSON, stripTrailingSlash } from '@/_helpers/utils';
import { withTranslation } from 'react-i18next';
import _ from 'lodash';
import { Link, Redirect } from 'react-router-dom';
import Spinner from '@/_ui/Spinner';
@ -30,13 +32,22 @@ class ViewerComponent extends React.Component {
const deviceWindowWidth = window.screen.width - 5;
const isMobileDevice = deviceWindowWidth < 600;
const pageHandle = this.props.match?.params?.pageHandle;
const slug = this.props.match.params.slug;
const appId = this.props.match.params.id;
const versionId = this.props.match.params.versionId;
this.state = {
slug,
appId,
versionId,
deviceWindowWidth,
currentLayout: isMobileDevice ? 'mobile' : 'desktop',
currentUser: authenticationService.currentUserValue,
isLoading: true,
users: null,
appDefinition: { components: {} },
appDefinition: { pages: {} },
currentState: {
queries: {},
components: {},
@ -45,22 +56,35 @@ class ViewerComponent extends React.Component {
theme: { name: props.darkMode ? 'dark' : 'light' },
urlparams: {},
environment_variables: {},
page: {
handle: pageHandle,
},
},
variables: {},
},
queryConfirmationList: [],
isAppLoaded: false,
errorAppId: null,
errorVersionId: null,
errorDetails: null,
pages: {},
};
}
setStateForApp = (data) => {
const copyDefinition = _.cloneDeep(data.definition);
const pagesObj = copyDefinition.pages || {};
const newDefinition = {
...copyDefinition,
pages: pagesObj,
};
this.setState({
app: data,
isLoading: false,
isAppLoaded: true,
appDefinition: data.definition || { components: {} },
appDefinition: newDefinition || { components: {} },
});
};
@ -80,10 +104,10 @@ class ViewerComponent extends React.Component {
let mobileLayoutHasWidgets = false;
if (this.state.currentLayout === 'mobile') {
const currentComponents = data.definition.pages[data.definition.homePageId].components;
mobileLayoutHasWidgets =
Object.keys(data.definition.components).filter(
(componentId) => data.definition.components[componentId]['layouts']['mobile']
).length > 0;
Object.keys(currentComponents).filter((componentId) => currentComponents[componentId]['layouts']['mobile'])
.length > 0;
}
let queryState = {};
@ -103,6 +127,12 @@ class ViewerComponent extends React.Component {
const variables = await this.fetchOrgEnvironmentVariables(data.slug, data.is_public);
const pages = Object.entries(data.definition.pages).map(([pageId, page]) => ({ id: pageId, ...page }));
const homePageId = data.definition.homePageId;
const startingPageHandle = this.props.match?.params?.pageHandle;
const currentPageId = pages.filter((page) => page.handle === startingPageHandle)[0]?.id ?? homePageId;
const currentPage = pages.find((page) => page.id === currentPageId);
this.setState(
{
currentSidebarTab: 2,
@ -122,12 +152,20 @@ class ViewerComponent extends React.Component {
theme: { name: this.props.darkMode ? 'dark' : 'light' },
urlparams: JSON.parse(JSON.stringify(queryString.parse(this.props.location.search))),
},
variables: {},
page: {
handle: currentPage.handle,
name: currentPage.name,
variables: {},
},
...variables,
},
dataQueries: data.data_queries,
currentPageId: currentPage.id,
pages: {},
},
() => {
computeComponentState(this, data?.definition?.components).then(() => {
computeComponentState(this, data?.definition?.pages[currentPage.id]?.components).then(() => {
console.log('Default component state computed and set');
this.runQueries(data.data_queries);
});
@ -249,6 +287,63 @@ class ViewerComponent extends React.Component {
this.setState({ isLoading: true });
this.loadApplicationBySlug(this.props.match.params.slug);
}
this.handlePageSwitchingBasedOnURLparam();
}
handlePageSwitchingBasedOnURLparam() {
const handleOnURL = this.props.match.params.pageHandle;
const pageIdCorrespondingToHandleOnURL = handleOnURL
? this.findPageIdFromHandle(handleOnURL)
: this.state.appDefinition.homePageId;
const currentPageId = this.state.currentPageId;
if (pageIdCorrespondingToHandleOnURL != this.state.currentPageId) {
const targetPage = this.state.appDefinition.pages[pageIdCorrespondingToHandleOnURL];
this.setState(
{
pages: {
...this.state.pages,
[currentPageId]: {
...this.state.pages?.[currentPageId],
variables: {
...this.state.currentState?.page?.variables,
},
},
},
currentPageId: pageIdCorrespondingToHandleOnURL,
handle: targetPage.handle,
name: targetPage.name,
currentState: {
...this.state.currentState,
globals: {
...this.state.currentState.globals,
urlparams: JSON.parse(JSON.stringify(queryString.parse(this.props.location.search))),
},
page: {
...this.state.currentState.page,
name: targetPage.name,
handle: targetPage.handle,
variables: this.state.pages?.[pageIdCorrespondingToHandleOnURL]?.variables ?? {},
},
},
},
async () => {
computeComponentState(this, this.state.appDefinition?.pages[this.state.currentPageId].components).then(
async () => {
const { events } = this.state.appDefinition?.pages[this.state.currentPageId];
for (const event of events ?? []) {
await this.handleEvent(event.eventId, event);
}
}
);
}
);
}
}
findPageIdFromHandle(handle) {
return Object.entries(this.state.appDefinition.pages).filter(([_id, page]) => page.handle === handle)?.[0]?.[0];
}
getCanvasWidth = () => {
@ -284,6 +379,38 @@ class ViewerComponent extends React.Component {
this.props.switchDarkMode(newMode);
};
switchPage = (id, queryParams = []) => {
const { handle, name, events } = this.state.appDefinition.pages[id];
const queryParamsString = queryParams.map(([key, value]) => `${key}=${value}`).join('&');
const { globals: existingGlobals } = this.state.currentState;
const globals = {
...existingGlobals,
urlparams: JSON.parse(JSON.stringify(queryString.parse(queryParamsString))),
};
if (this.state.slug) this.props.history.push(`/applications/${this.state.slug}/${handle}?${queryParamsString}`);
else
this.props.history.push(
`/applications/${this.state.appId}/versions/${this.state.versionId}/${handle}?${queryParamsString}`
);
};
handleEvent = (eventName, options) => onEvent(this, eventName, options, 'view');
computeCanvasMaxWidth = () => {
const { appDefinition } = this.state;
let computedCanvasMaxWidth = 1292;
if (appDefinition.globalSettings?.canvasMaxWidthType === 'px')
computedCanvasMaxWidth =
(+appDefinition.globalSettings?.canvasMaxWidth || 1292) - (appDefinition?.showViewerNavigation ? 200 : 0);
else if (appDefinition.globalSettings?.canvasMaxWidthType === '%')
computedCanvasMaxWidth = +appDefinition.globalSettings?.canvasMaxWidth + '%';
return computedCanvasMaxWidth;
};
render() {
const {
appDefinition,
@ -292,15 +419,19 @@ class ViewerComponent extends React.Component {
currentLayout,
deviceWindowWidth,
defaultComponentStateComputed,
canvasWidth,
dataQueries,
queryConfirmationList,
errorAppId,
errorVersionId,
errorDetails,
canvasWidth,
} = this.state;
if (isLoading) {
const currentCanvasWidth = canvasWidth;
const canvasMaxWidth = this.computeCanvasMaxWidth();
if (this.state.app?.isLoading) {
return (
<div className="tooljet-logo-loader">
<div>
@ -339,75 +470,83 @@ class ViewerComponent extends React.Component {
key={queryConfirmationList[0]?.queryName}
/>
<DndProvider backend={HTML5Backend}>
{!appDefinition.globalSettings?.hideHeader && isAppLoaded && (
<div className="header">
<header className="navbar navbar-expand-md navbar-light d-print-none">
<div className="container-xl header-container">
<h1 className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0">
<Link to="/" data-cy="viewer-page-logo">
<LogoIcon />
</Link>
</h1>
{this.state.app && <span>{this.state.app.name}</span>}
<div className="d-flex align-items-center m-1 p-1">
<DarkModeToggle switchDarkMode={this.changeDarkMode} darkMode={this.props.darkMode} />
</div>
</div>
</header>
</div>
)}
<ViewerNavigation.Header
showHeader={!appDefinition.globalSettings?.hideHeader && isAppLoaded}
appName={this.state.app?.name ?? null}
changeDarkMode={this.changeDarkMode}
darkMode={this.props.darkMode}
pages={Object.entries(this.state.appDefinition?.pages) ?? []}
currentPageId={this.state?.currentPageId ?? this.state.appDefinition?.homePageId}
switchPage={this.switchPage}
currentLayout={this.state.currentLayout}
/>
<div className="sub-section">
<div className="main">
<div className="canvas-container align-items-center">
<div
className="canvas-area"
style={{
width: canvasWidth,
minHeight: +appDefinition.globalSettings?.canvasMaxHeight || 2400,
maxWidth: +appDefinition.globalSettings?.canvasMaxWidth || 1292,
maxHeight: +appDefinition.globalSettings?.canvasMaxHeight || 2400,
backgroundColor: this.computeCanvasBackgroundColor(),
}}
>
{defaultComponentStateComputed && (
<>
{isLoading ? (
<div className="mx-auto mt-5 w-50 p-5">
<center>
<div className="spinner-border text-azure" role="status"></div>
</center>
</div>
) : (
<Container
appDefinition={appDefinition}
appDefinitionChanged={() => false} // function not relevant in viewer
snapToGrid={true}
appLoading={isLoading}
darkMode={this.props.darkMode}
onEvent={(eventName, options) => onEvent(this, eventName, options, 'view')}
mode="view"
deviceWindowWidth={deviceWindowWidth}
currentLayout={currentLayout}
currentState={this.state.currentState}
selectedComponent={this.state.selectedComponent}
onComponentClick={(id, component) => {
this.setState({
selectedComponent: { id, component },
});
onComponentClick(this, id, component, 'view');
}}
onComponentOptionChanged={(component, optionName, value) => {
return onComponentOptionChanged(this, component, optionName, value);
}}
onComponentOptionsChanged={(component, options) =>
onComponentOptionsChanged(this, component, options)
}
canvasWidth={this.getCanvasWidth()}
dataQueries={dataQueries}
/>
)}
</>
<div className="areas d-flex flex-rows justify-content-center">
{appDefinition?.showViewerNavigation && (
<ViewerNavigation
isMobileDevice={this.state.currentLayout === 'mobile'}
canvasBackgroundColor={this.computeCanvasBackgroundColor()}
pages={Object.entries(this.state.appDefinition?.pages) ?? []}
currentPageId={this.state?.currentPageId ?? this.state.appDefinition?.homePageId}
switchPage={this.switchPage}
darkMode={this.props.darkMode}
/>
)}
<div
className="canvas-area"
style={{
width: currentCanvasWidth,
minHeight: +appDefinition.globalSettings?.canvasMaxHeight || 2400,
maxWidth: canvasMaxWidth,
maxHeight: +appDefinition.globalSettings?.canvasMaxHeight || 2400,
backgroundColor: this.computeCanvasBackgroundColor(),
margin: 0,
padding: 0,
}}
>
{defaultComponentStateComputed && (
<>
{isLoading ? (
<div className="mx-auto mt-5 w-50 p-5">
<center>
<div className="spinner-border text-azure" role="status"></div>
</center>
</div>
) : (
<Container
appDefinition={appDefinition}
appDefinitionChanged={() => false} // function not relevant in viewer
snapToGrid={true}
appLoading={isLoading}
darkMode={this.props.darkMode}
onEvent={(eventName, options) => onEvent(this, eventName, options, 'view')}
mode="view"
deviceWindowWidth={deviceWindowWidth}
currentLayout={currentLayout}
currentState={this.state.currentState}
selectedComponent={this.state.selectedComponent}
onComponentClick={(id, component) => {
this.setState({
selectedComponent: { id, component },
});
onComponentClick(this, id, component, 'view');
}}
onComponentOptionChanged={(component, optionName, value) => {
return onComponentOptionChanged(this, component, optionName, value);
}}
onComponentOptionsChanged={(component, options) =>
onComponentOptionsChanged(this, component, options)
}
canvasWidth={this.getCanvasWidth()}
dataQueries={dataQueries}
currentPageId={this.state.currentPageId}
/>
)}
</>
)}
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,13 @@
import React from 'react';
const Header = ({ children, className }) => {
return (
<div className={`header ${className}`}>
<header className="navbar navbar-expand-md navbar-light d-print-none">
<div className="container-xl header-container position-relative">{children}</div>
</header>
</div>
);
};
export default Header;

View file

@ -0,0 +1,213 @@
import React from 'react';
import _ from 'lodash';
import { slide as Menu } from 'react-burger-menu';
import LogoIcon from '../Icons/logo.svg';
import { Link } from 'react-router-dom';
import { DarkModeToggle } from '@/_components/DarkModeToggle';
import Header from './Header';
export const ViewerNavigation = ({
isMobileDevice,
canvasBackgroundColor,
pages,
currentPageId,
switchPage,
darkMode,
}) => {
if (isMobileDevice) {
return null;
}
return (
<div
className={`navigation-area ${darkMode && 'dark'}`}
style={{
width: 200,
// backgroundColor: canvasBackgroundColor,
}}
>
<div className="page-handler-wrapper">
{pages.map(
([id, page]) =>
!page.hidden && (
<div
key={page.handle}
onClick={() => switchPage(id)}
className={`viewer-page-handler cursor-pointer ${darkMode && 'dark'}`}
>
<div className={`card mb-1 ${id === currentPageId ? 'active' : ''}`}>
<div className="card-body">
<span className="mx-3">{_.truncate(page.name, { length: 22 })}</span>
</div>
</div>
</div>
)
)}
</div>
</div>
);
};
const MobileNavigationMenu = ({ pages, switchPage, currentPageId, darkMode, changeDarkMode }) => {
const [hamburgerMenuOpen, setHamburgerMenuOpen] = React.useState(false);
const handlepageSwitch = (pageId) => {
setHamburgerMenuOpen(false);
switchPage(pageId);
};
var styles = {
bmBurgerButton: {
position: 'fixed',
width: '21px',
height: '16px',
right: 10,
top: 18,
},
bmBurgerBars: {
background: darkMode ? '#4C5155' : 'rgb(77, 114, 250)',
},
bmCrossButton: {
display: 'none',
},
bmCross: {
background: '#bdc3c7',
},
bmMenuWrap: {
height: '100%',
width: '100%',
top: 0,
},
bmMenu: {
background: darkMode ? '#202B37' : '#fff',
padding: '0',
},
bmMorphShape: {
fill: '#373a47',
},
bmItemList: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
bmItem: {
display: 'inline-block',
},
bmOverlay: {
background: 'rgba(0, 0, 0, 0.3)',
},
};
return (
<>
<Menu
isOpen={hamburgerMenuOpen}
styles={styles}
pageWrapId={'page-wrap'}
outerContainerId={'outer-container'}
onStateChange={(state) => setHamburgerMenuOpen(state.isOpen)}
right
>
<Header className={'mobile-header'}>
<div className="py-2 row w-100">
<div onClick={() => setHamburgerMenuOpen(false)} className="col-1 mx-1">
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.0507812" width="20" height="20" rx="4" fill="#F0F4FF"></rect>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.52851 5.57942C5.78886 5.31907 6.21097 5.31907 6.47132 5.57942L9.99992 9.10801L13.5285 5.57942C13.7889 5.31907 14.211 5.31907 14.4713 5.57942C14.7317 5.83977 14.7317 6.26188 14.4713 6.52223L10.9427 10.0508L14.4713 13.5794C14.7317 13.8398 14.7317 14.2619 14.4713 14.5222C14.211 14.7826 13.7889 14.7826 13.5285 14.5222L9.99992 10.9936L6.47132 14.5222C6.21097 14.7826 5.78886 14.7826 5.52851 14.5222C5.26816 14.2619 5.26816 13.8398 5.52851 13.5794L9.05711 10.0508L5.52851 6.52223C5.26816 6.26188 5.26816 5.83977 5.52851 5.57942Z"
fill="#3E63DD"
></path>
</svg>
</div>
<div style={{ marginTop: '2px' }} className="col">
<span>Menu</span>
</div>
</div>
</Header>
<div className="p-2 w-100">
<div className={`pages-container ${darkMode && 'dark'}`}>
{pages.map(
([id, page]) =>
!page.hidden && (
<div
key={page.handle}
onClick={() => handlepageSwitch(id)}
className={`viewer-page-handler mb-2 cursor-pointer ${darkMode && 'dark'}`}
>
<div className={`card mb-1 ${id === currentPageId ? 'active' : ''}`}>
<div className="card-body">
<span className="mx-3">{_.truncate(page.name, { length: 22 })}</span>
</div>
</div>
</div>
)
)}
</div>
</div>
<ViewerNavigation.Footer darkMode={darkMode} switchDarkMode={changeDarkMode} />
</Menu>
</>
);
};
const ViewerHeader = ({
showHeader,
appName,
changeDarkMode,
darkMode,
pages,
currentPageId,
switchPage,
currentLayout,
}) => {
return (
<Header>
{showHeader && (
<>
<h1 className="navbar-brand navbar-brand-autodark d-none-navbar-horizontal pe-0">
<Link to="/" data-cy="viewer-page-logo">
<LogoIcon />
</Link>
</h1>
{appName && <span>{appName}</span>}
</>
)}
<div
style={{
width: '60px',
}}
className={`d-flex align-items-center m-1 p-1`}
>
<DarkModeToggle switchDarkMode={changeDarkMode} darkMode={darkMode} />
{currentLayout === 'mobile' && (
<ViewerNavigation.BurgerMenu
pages={pages}
currentPageId={currentPageId}
switchPage={switchPage}
darkMode={darkMode}
changeDarkMode={changeDarkMode}
/>
)}
</div>
</Header>
);
};
const Footer = ({ darkMode, switchDarkMode }) => {
return (
<div className="viewer-footer fixed-bottom">
<footer className="border-top">
<div className={`d-flex align-items-center p-2 mx-3 position-absolute`}>
<DarkModeToggle switchDarkMode={switchDarkMode} darkMode={darkMode} showText={true} />
</div>
</footer>
</div>
);
};
ViewerNavigation.BurgerMenu = MobileNavigationMenu;
ViewerNavigation.Header = ViewerHeader;
ViewerNavigation.Footer = Footer;

View file

@ -16,6 +16,9 @@ const universalProps = {
others: {},
events: [],
styles: {},
generalStyles: {
boxShadow: { value: '0px 0px 0px 0px #00000040' },
},
},
};

View file

@ -8,6 +8,7 @@ export const DarkModeToggle = function DarkModeToggle({
darkMode = false,
switchDarkMode,
tooltipPlacement = 'bottom',
showText = false,
}) {
const toggleDarkMode = () => {
switchDarkMode(!darkMode);
@ -58,46 +59,51 @@ export const DarkModeToggle = function DarkModeToggle({
</Tooltip>
}
>
<animated.svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
color={darkMode ? '#fff' : '#808080'}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke="currentColor"
onClick={toggleDarkMode}
style={{
cursor: 'pointer',
...svgContainerProps,
}}
>
<mask id="myMask2">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<animated.circle style={maskedCircleProps} r="9" fill="black" />
</mask>
<div className="unstyled-button dark-theme-toggle-btn" onClick={toggleDarkMode}>
<animated.svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
color={darkMode ? '#fff' : '#808080'}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
stroke="currentColor"
style={{
cursor: 'pointer',
...svgContainerProps,
}}
>
<mask id="myMask2">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<animated.circle style={maskedCircleProps} r="9" fill="black" />
</mask>
<animated.circle
cx="12"
cy="12"
style={centerCircleProps}
fill={darkMode ? 'white' : '#808080'}
mask="url(#myMask2)"
/>
<animated.g stroke="currentColor" style={linesProps}>
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</animated.g>
</animated.svg>
<animated.circle
cx="12"
cy="12"
style={centerCircleProps}
fill={darkMode ? 'white' : '#808080'}
mask="url(#myMask2)"
/>
<animated.g stroke="currentColor" style={linesProps}>
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</animated.g>
</animated.svg>
{showText && (
<span className="dark-theme-toggle-btn-text">Switch to {!darkMode ? 'dark mode' : 'light mode'}</span>
)}
</div>
</OverlayTrigger>
);
};

View file

@ -0,0 +1,48 @@
import React, { useMemo, useState } from 'react';
import { DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, arrayMove, sortableKeyboardCoordinates } from '@dnd-kit/sortable';
import { SortableItem, SortableOverlay } from './components';
export function SortableList({ items, onChange, renderItem }) {
const [active, setActive] = useState(null);
const activeItem = useMemo(() => items.find((item) => item.id === active?.id), [active, items]);
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
return (
<DndContext
sensors={sensors}
onDragStart={({ active }) => {
setActive(active);
}}
onDragEnd={({ active, over }) => {
if (over && active.id !== over?.id) {
const activeIndex = items.findIndex(({ id }) => id === active.id);
const overIndex = items.findIndex(({ id }) => id === over.id);
onChange(arrayMove(items, activeIndex, overIndex));
}
setActive(null);
}}
onDragCancel={() => {
setActive(null);
}}
>
<SortableContext items={items}>
{items.map((item) => (
<React.Fragment key={item.id}>{renderItem(item)}</React.Fragment>
))}
</SortableContext>
<SortableOverlay>{activeItem ? renderItem(activeItem) : null}</SortableOverlay>
</DndContext>
);
}
SortableList.Item = SortableItem;

View file

@ -0,0 +1,53 @@
import React, { createContext, useContext, useMemo } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
const SortableItemContext = createContext({
attributes: {},
listeners: undefined,
ref() {},
});
export function SortableItem({ children, id, classNames }) {
const { attributes, isDragging, listeners, setNodeRef, setActivatorNodeRef, transform, transition } = useSortable({
id,
});
const context = useMemo(
() => ({
attributes,
listeners,
ref: setActivatorNodeRef,
}),
[attributes, listeners, setActivatorNodeRef]
);
const style = {
opacity: isDragging ? 0.4 : undefined,
transform: CSS.Translate.toString(transform),
transition,
};
return (
<SortableItemContext.Provider value={context}>
<div className={classNames} ref={setNodeRef} style={style}>
{children}
</div>
</SortableItemContext.Provider>
);
}
export function DragHandle({ show = true }) {
const { attributes, listeners, ref } = useContext(SortableItemContext);
if (!show) {
return <span className="DragHandle"></span>;
}
return (
<button className="DragHandle" {...attributes} {...listeners} ref={ref}>
<svg viewBox="0 0 20 20" width="12">
<path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"></path>
</svg>
</button>
);
}

View file

@ -0,0 +1,16 @@
import React from 'react';
import { DragOverlay, defaultDropAnimationSideEffects } from '@dnd-kit/core';
const dropAnimationConfig = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: '0.4',
},
},
}),
};
export function SortableOverlay({ children }) {
return <DragOverlay dropAnimation={dropAnimationConfig}>{children}</DragOverlay>;
}

View file

@ -0,0 +1,4 @@
import { SortableItem, DragHandle } from './SortableItem';
import { SortableOverlay } from './SortableOverlay';
export { SortableItem, SortableOverlay, DragHandle };

View file

@ -0,0 +1,47 @@
import React from 'react';
import { SortableList } from './SortableList';
import { DragHandle } from './components';
const SortableComponent = ({ data, Element, ...restProps }) => {
const { onSort } = restProps;
const [items, setItems] = React.useState([]);
React.useEffect(() => {
setItems(data);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(data)]);
//function to check if the item in items array has changed position with respect to the original data
const didItemChangePosition = (originalArr, sortedArry) => {
return originalArr.some((item, index) => {
return item.id !== sortedArry[index].id;
});
};
React.useEffect(() => {
if (items.length > 0 && didItemChangePosition(data, items)) {
console.log('items changed ==>');
onSort(items);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [items]);
return (
<div style={{ maxWidth: 400, margin: '0' }}>
<SortableList
items={items}
onChange={setItems}
renderItem={(page) => (
<SortableList.Item id={page.id} classNames={restProps.classNames}>
<Element page={page} {...restProps} />
</SortableList.Item>
)}
/>
</div>
);
};
SortableComponent.DragHandle = DragHandle;
export default SortableComponent;

View file

@ -110,6 +110,7 @@ async function exceutePycode(payload, code, currentState, query, mode) {
variables = currentState['variables']
client = currentState['client']
server = currentState['server']
page = currentState['page']
code_to_execute = ${_code}
try:
@ -186,7 +187,7 @@ export async function runTransformation(
if (transformationLanguage === 'javascript') {
try {
const evalFunction = Function(
['data', 'moment', '_', 'components', 'queries', 'globals', 'variables'],
['data', 'moment', '_', 'components', 'queries', 'globals', 'variables', 'page'],
transformation
);
@ -197,7 +198,8 @@ export async function runTransformation(
currentState.components,
currentState.queries,
currentState.globals,
currentState.variables
currentState.variables,
currentState.page
);
} catch (err) {
console.log('Transformation failed for query: ', query.name, err);
@ -212,7 +214,7 @@ export async function runTransformation(
}
export async function executeActionsForEventId(_ref, eventId, component, mode, customVariables) {
const events = component.definition.events || [];
const events = component?.definition?.events || [];
const filteredEvents = events.filter((event) => event.eventId === eventId);
for (const event of filteredEvents) {
@ -251,7 +253,7 @@ function showModal(_ref, modal, show) {
return Promise.resolve();
}
const modalMeta = _ref.state.appDefinition.components[modalId];
const modalMeta = _ref.state.appDefinition.pages[_ref.state.currentPageId].components[modalId];
const newState = {
currentState: {
..._ref.state.currentState,
@ -422,6 +424,37 @@ export const executeAction = (_ref, event, mode, customVariables) => {
});
}
case 'set-page-variable': {
const key = resolveReferences(event.key, _ref.state.currentState, undefined, customVariables);
const value = resolveReferences(event.value, _ref.state.currentState, undefined, customVariables);
const customPageVariables = { ..._ref.state.currentState.page.variables, [key]: value };
return _ref.setState({
currentState: {
..._ref.state.currentState,
page: {
..._ref.state.currentState.page,
variables: customPageVariables,
},
},
});
}
case 'unset-page-variable': {
const key = resolveReferences(event.key, _ref.state.currentState, undefined, customVariables);
const customPageVariables = _.omit(_ref.state.currentState.page.variables, key);
return _ref.setState({
currentState: {
..._ref.state.currentState,
page: {
..._ref.state.currentState.page,
variables: customPageVariables,
},
},
});
}
case 'control-component': {
const component = Object.values(_ref.state.currentState?.components ?? {}).filter(
(component) => component.id === event.componentId
@ -434,6 +467,14 @@ export const executeAction = (_ref, event, mode, customVariables) => {
const actionPromise = action(...actionArguments.map((argument) => argument.value));
return actionPromise ?? Promise.resolve();
}
case 'switch-page': {
_ref.switchPage(
event.pageId,
resolveReferences(event.queryParams, _ref.state.currentState, [], customVariables)
);
return Promise.resolve();
}
}
}
};
@ -444,6 +485,10 @@ export async function onEvent(_ref, eventName, options, mode = 'edit') {
const { customVariables } = options;
if (eventName === 'onPageLoad') {
await executeActionsForEventId(_ref, 'onPageLoad', { definition: { events: [options] } }, mode, customVariables);
}
if (eventName === 'onTrigger') {
const { component, queryId, queryName } = options;
_self.setState(
@ -1042,7 +1087,9 @@ export const debuggerActions = {
key,
type: value.type,
kind: errorType !== 'transformations' ? value.kind : 'transformations',
page: value.page,
timestamp: moment(),
strace: value.strace ?? 'app_level',
};
switch (errorType) {
@ -1097,19 +1144,22 @@ export const getComponentName = (currentState, id) => {
}
};
const updateNewComponents = (appDefinition, newComponents, updateAppDefinition) => {
const updateNewComponents = (pageId, appDefinition, newComponents, updateAppDefinition) => {
const newAppDefinition = JSON.parse(JSON.stringify(appDefinition));
newComponents.forEach((newComponent) => {
newComponent.component.name = computeComponentName(newComponent.component.component, newAppDefinition.components);
newAppDefinition.components[newComponent.id] = newComponent;
newComponent.component.name = computeComponentName(
newComponent.component.component,
newAppDefinition.pages[pageId].components
);
newAppDefinition.pages[pageId].components[newComponent.id] = newComponent;
});
updateAppDefinition(newAppDefinition);
};
export const cloneComponents = (_ref, updateAppDefinition, isCloning = true, isCut = false) => {
const { selectedComponents, appDefinition } = _ref.state;
const { selectedComponents, appDefinition, currentPageId } = _ref.state;
if (selectedComponents.length < 1) return getSelectedText();
const { components: allComponents } = appDefinition;
const { components: allComponents } = appDefinition.pages[currentPageId];
let newDefinition = _.cloneDeep(appDefinition);
let newComponents = [],
newComponentObj = {},
@ -1135,11 +1185,11 @@ export const cloneComponents = (_ref, updateAppDefinition, isCloning = true, isC
};
}
if (isCloning) {
addComponents(appDefinition, updateAppDefinition, undefined, newComponentObj);
addComponents(currentPageId, appDefinition, updateAppDefinition, undefined, newComponentObj);
toast.success('Component cloned succesfully');
} else if (isCut) {
navigator.clipboard.writeText(JSON.stringify(newComponentObj));
removeSelectedComponent(newDefinition, selectedComponents);
removeSelectedComponent(currentPageId, newDefinition, selectedComponents);
updateAppDefinition(newDefinition);
} else {
navigator.clipboard.writeText(JSON.stringify(newComponentObj));
@ -1205,14 +1255,15 @@ const updateComponentLayout = (components, parentId, isCut = false) => {
});
};
export const addComponents = (appDefinition, appDefinitionChanged, parentId = undefined, newComponentObj) => {
export const addComponents = (pageId, appDefinition, appDefinitionChanged, parentId = undefined, newComponentObj) => {
console.log({ pageId, newComponentObj });
const finalComponents = [];
let parentComponent = undefined;
const { isCloning, isCut, newComponents: pastedComponent = [] } = newComponentObj;
if (parentId) {
const id = Object.keys(appDefinition.components).filter((key) => parentId.startsWith(key));
parentComponent = JSON.parse(JSON.stringify(appDefinition.components[id[0]]));
const id = Object.keys(appDefinition.pages[pageId].components).filter((key) => parentId.startsWith(key));
parentComponent = JSON.parse(JSON.stringify(appDefinition.pages[pageId].components[id[0]]));
parentComponent.id = parentId;
}
@ -1247,7 +1298,7 @@ export const addComponents = (appDefinition, appDefinitionChanged, parentId = un
buildComponents(pastedComponent, parentComponent, true);
updateNewComponents(appDefinition, finalComponents, appDefinitionChanged);
updateNewComponents(pageId, appDefinition, finalComponents, appDefinitionChanged);
!isCloning && toast.success('Component pasted succesfully');
};
@ -1340,25 +1391,25 @@ export function snapToGrid(canvasWidth, x, y) {
const snappedY = Math.round(y / 10) * 10;
return [snappedX, snappedY];
}
export const removeSelectedComponent = (newDefinition, selectedComponents) => {
export const removeSelectedComponent = (pageId, newDefinition, selectedComponents) => {
selectedComponents.forEach((component) => {
let childComponents = [];
if (newDefinition.components[component.id]?.component?.component === 'Tabs') {
childComponents = Object.keys(newDefinition.components).filter((key) =>
newDefinition.components[key].parent?.startsWith(component.id)
if (newDefinition.pages[pageId].components[component.id]?.component?.component === 'Tabs') {
childComponents = Object.keys(newDefinition.pages[pageId].components).filter((key) =>
newDefinition.pages[pageId].components[key].parent?.startsWith(component.id)
);
} else {
childComponents = Object.keys(newDefinition.components).filter(
(key) => newDefinition.components[key].parent === component.id
childComponents = Object.keys(newDefinition.pages[pageId].components).filter(
(key) => newDefinition.pages[pageId].components[key].parent === component.id
);
}
childComponents.forEach((componentId) => {
delete newDefinition.components[componentId];
delete newDefinition.pages[pageId].components[componentId];
});
delete newDefinition.components[component.id];
delete newDefinition.pages[pageId].components[component.id];
});
};

View file

@ -56,6 +56,7 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
'components',
'queries',
'globals',
'page',
'client',
'server',
'moment',
@ -70,6 +71,7 @@ function resolveCode(code, state, customObjects = {}, withError = false, reserve
isJsCode ? state?.components : undefined,
isJsCode ? state?.queries : undefined,
isJsCode ? state?.globals : undefined,
isJsCode ? state?.page : undefined,
isJsCode ? undefined : state?.client,
isJsCode ? undefined : state?.server,
moment,
@ -470,6 +472,7 @@ export async function executeMultilineJS(
'components',
'queries',
'globals',
'page',
'axios',
'variables',
'actions',
@ -483,6 +486,7 @@ export async function executeMultilineJS(
currentState.components,
currentState.queries,
currentState.globals,
currentState.page,
axios,
currentState.variables,
actions

View file

@ -36,8 +36,10 @@ function deleteComment(commentId) {
return adapter.delete(`/comments/${commentId}`);
}
function getNotifications(appId, isResolved, appVersionsId) {
return adapter.get(`/comments/${appId}/notifications?isResolved=${isResolved}&appVersionsId=${appVersionsId}`);
function getNotifications(appId, isResolved, appVersionsId, pageId) {
return adapter.get(
`/comments/${appId}/notifications?isResolved=${isResolved}&appVersionsId=${appVersionsId}&pageId=${pageId}`
);
}
export const commentsService = {

View file

@ -0,0 +1,298 @@
$base-border-radius: 6px;
$btn-bg: #FFFFFF;
$btn-color: #11181C;
$btn-dark-bg: #121212;
$btn-dark-color: #FFFFFF;
@mixin button($bg, $color) {
background-color: $bg;
color: $color;
border-radius: $base-border-radius;
border: none;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease-in-out;
&:hover {
@if $bg != none {
background-color: darken($bg, 10%);
}
}
}
.base-button {
@include button($btn-bg, $btn-color);
border-radius: $base-border-radius;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
padding: 4px 16px;
border: 1px solid #D7DBDF;
}
.base-button.dark {
background: $btn-dark-bg;
color: $btn-dark-color;
border-color: #11181C;
&:hover {
background: lighten($btn-dark-bg, 10%);
}
img {
filter: brightness(0) invert(1);
}
}
.unstyled-button {
@include button(none, inherit);
border: none;
font-size: 12px;
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
line-height: 20px;
}
.page-handle-button-container {
border-radius: $base-border-radius;
display: flex;
flex-direction: row;
justify-content: left;
align-items: center;
padding: 6px 8px;
border: 1px solid #D7DBDF;
height: 32px;
img {
position: absolute;
right: 0 !important;
margin-right: 1.5rem !important;
filter: invert(38%) sepia(85%) saturate(5221%) hue-rotate(217deg) brightness(91%) contrast(90%);
}
}
.popover-dark-themed .page-handle-button-container {
border-color: #697177;
img {
filter: invert(95%) sepia(38%) saturate(4716%) hue-rotate(180deg) brightness(113%) contrast(102%);
}
}
.leftsidebar-panel-header {
height: 100%;
background-color: #F1F3F5;
.panel-header-container {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
height: 52px;
border-bottom: 1px solid #E6E8EB;
.add-new-page {
margin-left: 4px;
}
}
.panel-search-container {
padding: 8px 12px;
border-bottom: 1px solid #E6E8EB;
}
}
.leftsidebar-panel-header.dark {
// background-color: #202425;
background-color: #1F2936;
.panel-header-container, .panel-search-container {
border-color: #697177;
}
}
.page-selector-panel-body {
height: 100%;
padding: 12px;
background-color: #FFFFFF;
.page-handler {
height: 32px !important;
padding: 0;
margin-bottom: 6px;
font-weight: 500;
.card, .card-body {
padding: 3px;
height: 32px;
}
.card.active {
background: #ECEEF0;
}
.card.non-active-page {
border: none;
box-shadow: none;
}
.card:hover {
background: #E6EDFE !important;
}
.page-name-input {
height: 32px;
}
}
}
.page-selector-panel-body.dark {
background: #1F2936;
.page-handler {
.card {
background: none !important
}
.card.active {
background: #26292B !important;
}
.card:hover{
background: #2C3547 !important;
}
}
}
.left-sidebar-page-selector.dark {
background: #1F2936 !important;
.clear-icon {
filter: invert(100%);
}
}
#page-handler-menu.global-settings {
min-height: 124px;
padding: 6px;
}
#page-handler-menu {
border-radius: 4px;
width: 238px;
margin-top: 0.2rem !important;
margin-left: 0.5rem !important;
box-shadow: 0px 3px 2px rgba(0, 0, 0, 0.25);
.popover-body {
padding: 16px 6px 0px 6px;
height: 100%;
.card-body {
padding: 0;
height: 100%;
}
.field {
font-weight: 500;
font-size: 0.7rem;
&:hover {
color:#919eab;
}
&__danger {
color: #ff6666;
}
}
}
}
.page-icons {
position: relative;
left: 1rem;
}
.page-handler-alert {
background-color: #fff5f0!important;
border: 1px solid #FFF1E7!important;
}
.page-handle-edit-container {
height: 60px;
width: 100%;
.input-group {
height: 42px;
padding: 2px;
}
.input-group-text {
border: none;
background: none;
font-weight: 400;
font-size: 14px;
line-height: 20px;
padding-right: 4px;
}
.page-handler-input {
border-radius: $base-border-radius !important;
}
}
.page-handle-edit-modal.theme-dark {
background: none !important;
.input-group-text {
border: none !important;
background: none!important; ;
}
}
.page-handle-tip {
text-decoration: none!important;
}
.delete-btn.field__danger {
img {
filter: invert(37%) sepia(50%) saturate(2105%) hue-rotate(342deg) brightness(93%) contrast(93%);
}
}
.clear-icon {
margin-top: 6px;
}
.secondary-text {
color: #687076;
}
.DragHandle {
display: flex;
width: 12px;
padding: 3px;
align-items: center;
justify-content: center;
flex: 0 0 auto;
touch-action: none;
cursor: grab !important;
border-radius: 5px;
border: none;
outline: none;
appearance: none;
background-color: transparent;
-webkit-tap-highlight-color: transparent;
}
.DragHandle svg {
flex: 0 0 auto;
margin: auto;
height: 100%;
overflow: visible;
fill: #919eab;
}

View file

@ -68,9 +68,14 @@ div[data-disabled='true'] {
}
}
.switch-page {
.css-1nfapid-container{
width: 100%;
}
}
.loader-main-container{
width: 0px !important;
position: absolute;
bottom: 44%;
left: 46%;
}
}

View file

@ -38,7 +38,7 @@
max-height: 60%;
}
.datasources-popover {
top: 100px;
top: 200px;
width: 200px;
.add-btn {
border: none;
@ -46,7 +46,7 @@
}
}
.debugger-popover {
top: 180px;
top: 270px;
cursor: pointer;
}
.global-settings-popover {
@ -349,7 +349,6 @@
display: block;
position: absolute;
left: 0;
margin-right: 0.5rem;
}
.clear-icon {
@ -361,6 +360,12 @@
}
}
.panel-search-container {
.search-icon {
margin: 0.4rem !important;
}
}
.link-span {
text-decoration: none;
text-decoration: underline;
@ -398,3 +403,64 @@
left: 0;
}
}
.page-handler-wrapper {
background: transparent;
}
.viewer-page-handler {
height: 32px;
padding: 0;
// max-width: 185px;
.card {
background: none;
border: none;
box-shadow: none;
}
.card,.card-body {
padding: 3px;
height: 32px;
}
.card.active {
background: #fff;
}
}
.viewer-page-handler.dark {
.card.active {
background: #2c405c;
}
}
.theme-dark .viewer-page-handler .card {
background: transparent;
}
.mobile-header {
width: 100%;
}
.bm-item .pages-container {
width: 100% !important;
padding: 1rem 2rem;
.card.active {
width: inherit;
background-color: #ECEEF0;
color: #3e525b;
}
}
.bm-item .pages-container.dark {
.card.active {
background-color: #2c405c;
color: #fff;
}
}
.viewer-footer {
height: 48px;
}

View file

@ -2,6 +2,7 @@
@import "./colors.scss";
@import "./z-index.scss";
@import "./mixins.scss";
@import "./components.scss";
// variables
$border-radius: 4px;
@ -360,6 +361,13 @@ button {
padding: 1rem 0rem 1rem 1rem;
}
}
.left-sidebar-page-selector {
.add-new-page-button-container{
width: 100%;
margin-top: 10px;
}
}
}
.editor-sidebar {
@ -560,7 +568,7 @@ button {
}
.query-pane {
z-index: 1;
z-index: 10;
height: 350px;
position: fixed;
left: 76px; //sidebar is 76px
@ -769,6 +777,29 @@ button {
background-size: 80px 80px;
background-repeat: repeat;
}
.navigation-area {
background-color: #ECEEF0;
padding: 1rem;
a.page-link {
border-radius: 0;
border: 0;
}
a.page-link:hover {
color: white;
background-color: #4D72FA ;
}
a.page-link.active {
color: white;
background-color: #4D72FA ;
}
}
.navigation-area.dark {
background-color: #2b3546 !important;
}
}
}
}
@ -3034,7 +3065,7 @@ input:focus-visible {
}
.card {
background-color: #324156 !important;
background-color: #324156;
}
.card .table tbody td a {
@ -3076,6 +3107,25 @@ input:focus-visible {
background-color: #2f3c4c;
}
.main .navigation-area {
background-color: #2f3c4c !important;
a.page-link {
border-radius: 0;
border: 0;
color: white;
}
a.page-link:hover {
color: white;
background-color: #4D72FA ;
}
a.page-link.active {
color: white;
background-color: #4D72FA ;
}
}
.rdtOpen .rdtPicker {
color: black;
}
@ -3180,6 +3230,23 @@ input:focus-visible {
.text-muted {
color: #fff !important;
}
.left-sidebar-page-selector {
.list-group {
.list-group-item {
border: solid #1d2a39 1px;
color: white;
}
.list-group-item:hover {
background-color: #1F2936;
}
.list-group-item.active {
background-color: #1F2936;
}
}
}
}
.folder-list {
@ -4402,9 +4469,14 @@ input[type="text"] {
.searchbox-wrapper {
margin-top: 0 !important;
.search-icon {
margin: 0.30rem
}
input {
border-radius: $border-radius !important;
padding-left: 1.75rem !important;
}
}
@ -4904,7 +4976,7 @@ input[type="text"] {
// **Alert component**
.alert-component {
border: 1px solid rgba(101, 109, 119, 0.16) !important;
border: 1px solid rgba(101, 109, 119, 0.16);
background: #f5f7f9;
a {
@ -5866,6 +5938,10 @@ div#driver-page-overlay {
cursor: text;
}
.cursor-not-allowed {
cursor: none;
}
.bade-component {
display: inline-flex;
justify-content: center;
@ -6930,9 +7006,33 @@ tbody {
.page-body {
height: calc(100vh - 1.25rem - 48px);
min-height: 500px;
min-height : 500px;
}
.theme-dark{
.org-avatar:hover{
.avatar{
background: #10141A no-repeat center/cover ;
}
}
}
.dark-theme-toggle-btn {
height: 32px;
display: flex;
align-items: center;
justify-content: center;
}
.dark-theme-toggle-btn-text {
font-size: 14px;
margin: 12px;
}
.maximum-canvas-height-input-field {
width: 90px;
}
//ONBOARD START---------------------------->>>>>
.onboarding-page-wrapper {
@ -8179,4 +8279,4 @@ tbody {
}
//ONBOARD STYLES END---------------------------->>>>>
//ONBOARD STYLES END---------------------------->>>>>

View file

@ -0,0 +1,63 @@
import React from 'react';
const defaultDisabledStyles = {
color: '#C1C8CD',
cursor: 'not-allowed',
pointerEvents: 'none',
};
const Button = ({
children,
onClick,
darkMode,
size = 'sm',
classNames = '',
styles = {},
disabled = false,
isLoading = false,
}) => {
const baseHeight = size === 'sm' ? 28 : 40;
const baseWidth = size === 'sm' ? 92 : 150;
const diabledStyles = {
...defaultDisabledStyles,
backgroundColor: '#F1F3F5',
};
return (
<div
type="button"
style={{ height: baseHeight, width: baseWidth, ...styles, ...(disabled ? diabledStyles : {}) }}
className={`btn base-button m-1 ${darkMode && 'dark'} ${classNames} ${isLoading && 'btn-loading'}`}
onClick={onClick}
>
{!isLoading && children}
</div>
);
};
const Content = ({ title = null, iconSrc = null, direction = 'left' }) => {
const icon = !iconSrc ? '' : <img className="mx-1" src={iconSrc} width="12" height="12" />;
const btnTitle = !title ? '' : typeof title === 'function' ? title() : <span className="mx-1">{title}</span>;
const content = direction === 'left' ? [icon, btnTitle] : [btnTitle, icon];
return content;
};
const UnstyledButton = ({ children, onClick, classNames = '', styles = {}, disabled = false }) => {
return (
<div
type="button"
style={{ ...styles, ...(disabled ? defaultDisabledStyles : {}) }}
className={`unstyled-button ${classNames} ${disabled && 'disabled'}`}
onClick={onClick}
>
{children}
</div>
);
};
Button.Content = Content;
Button.UnstyledButton = UnstyledButton;
export default Button;

View file

@ -0,0 +1,35 @@
import React from 'react';
import { SearchBoxComponent } from '@/_ui/Search';
const Header = ({ children, darkMode }) => {
return <div className={`${darkMode && 'dark'} leftsidebar-panel-header`}>{children}</div>;
};
const PanelHeader = ({ children, title }) => {
return (
<div className="panel-header-container row">
<div className="col-3">
<p className="text-muted m-0 fw-500">{title}</p>
</div>
<div className="col-9 px-1">{children}</div>
</div>
);
};
const SearchContainer = ({ onChange, placeholder, placeholderIcon, callBack = null }) => {
return (
<div className="panel-search-container">
<SearchBoxComponent
onChange={onChange}
callback={callBack}
placeholder={placeholder}
placeholderIcon={placeholderIcon}
/>
</div>
);
};
Header.PanelHeader = PanelHeader;
Header.SearchBoxComponent = SearchContainer;
export default Header;

View file

@ -0,0 +1,4 @@
import Button from './Button';
import HeaderSection from './Header';
export { Button, HeaderSection };

View file

@ -2,7 +2,7 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
export const SearchBox = ({ onChange, ...restProps }) => {
const { callback, placeholder } = restProps;
const { callback, placeholder, placeholderIcon = null } = restProps;
const [searchText, setSearchText] = React.useState('');
const { t } = useTranslation();
@ -32,45 +32,43 @@ export const SearchBox = ({ onChange, ...restProps }) => {
<div className="searchbox-wrapper">
<div style={{ height: '32px' }} className="input-icon d-flex">
{searchText.length === 0 && (
<span className="search-icon mt-2 mx-2">
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="10" cy="10" r="7" />
<line x1="21" y1="21" x2="15" y2="15" />
<span className="search-icon mx-2" onClick={clearSearch}>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.34994 1.66683C3.1408 1.66683 1.34993 3.45769 1.34993 5.66683C1.34993 7.87597 3.1408 9.66683 5.34994 9.66683C7.55907 9.66683 9.34994 7.87597 9.34994 5.66683C9.34994 3.45769 7.55907 1.66683 5.34994 1.66683ZM0.0166016 5.66683C0.0166016 2.72131 2.40442 0.333496 5.34994 0.333496C8.29545 0.333496 10.6833 2.72131 10.6833 5.66683C10.6833 6.8993 10.2652 8.03414 9.56317 8.93726L13.1547 12.5288C13.415 12.7891 13.415 13.2112 13.1547 13.4716C12.8943 13.7319 12.4722 13.7319 12.2119 13.4716L8.62037 9.88007C7.71724 10.5821 6.58241 11.0002 5.34994 11.0002C2.40442 11.0002 0.0166016 8.61235 0.0166016 5.66683Z"
fill="#C1C8CD"
/>
</svg>
</span>
)}
{searchText.length > 0 && (
<span className="clear-icon mt-2" onClick={clearSearch}>
<svg
xmlns="http://www.w3.org/2000/svg"
className="icon icon-tabler icon-tabler-circle-x"
width="24"
height="24"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<circle cx="12" cy="12" r="9"></circle>
<path d="M10 10l4 4m0 -4l-4 4"></path>
<span className="clear-icon" onClick={clearSearch}>
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect y="0.0507812" width="20" height="20" rx="4" fill="#F0F4FF" />
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.52851 5.57942C5.78886 5.31907 6.21097 5.31907 6.47132 5.57942L9.99992 9.10801L13.5285 5.57942C13.7889 5.31907 14.211 5.31907 14.4713 5.57942C14.7317 5.83977 14.7317 6.26188 14.4713 6.52223L10.9427 10.0508L14.4713 13.5794C14.7317 13.8398 14.7317 14.2619 14.4713 14.5222C14.211 14.7826 13.7889 14.7826 13.5285 14.5222L9.99992 10.9936L6.47132 14.5222C6.21097 14.7826 5.78886 14.7826 5.52851 14.5222C5.26816 14.2619 5.26816 13.8398 5.52851 13.5794L9.05711 10.0508L5.52851 6.52223C5.26816 6.26188 5.26816 5.83977 5.52851 5.57942Z"
fill="#3E63DD"
/>
</svg>
</span>
)}
{placeholderIcon && searchText.length === 0 && (
// custom placeholder icon
<span
style={{
color: '#7E868C',
}}
className="clear-icon mt-2 cursor-not-allowed"
>
{placeholderIcon}
</span>
)}
{typeof callback === 'function' && searchText.length === 0 && (
<span className="clear-icon mt-2" onClick={callback}>
<svg

View file

@ -0,0 +1,36 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { AppVersion } from '../src/entities/app_version.entity';
import {
convertAppDefinitionFromSinglePageToMultiPage,
convertAppDefinitionFromMultiPageToSinglePage,
} from '../lib/single-page-to-and-from-multipage-definition-conversion';
export class ChangeDefinitionStructureForMultiPage1668521091918 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const entityManager = queryRunner.manager;
const appVersions = await entityManager.find(AppVersion);
for (const version of appVersions) {
const definition = version['definition'];
if (definition) {
const newDefinition = convertAppDefinitionFromSinglePageToMultiPage(definition);
version.definition = newDefinition;
await entityManager.update(AppVersion, { id: version.id }, { definition: newDefinition });
}
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const entityManager = queryRunner.manager;
const appVersions = await entityManager.find(AppVersion);
for (const version of appVersions) {
const definition = version['definition'];
if (definition) {
const newDefinition = convertAppDefinitionFromMultiPageToSinglePage(definition);
version.definition = newDefinition;
await entityManager.update(AppVersion, { id: version.id }, { definition: newDefinition });
}
}
}
}

View file

@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
import { Thread } from '../src/entities/thread.entity';
import { AppVersion } from '../src/entities/app_version.entity';
export class ConnectExistingCommentThreadsToPageIds1669293520796 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const entityManager = queryRunner.manager;
const threads = await entityManager.find(Thread);
for (const thread of threads) {
const appVersion = await entityManager.findOne(AppVersion, {
where: {
id: thread.appVersionsId,
},
});
if (appVersion?.definition) {
const pageId = Object.keys(appVersion.definition.pages)?.[0];
await entityManager.update(Thread, { id: thread.id }, { pageId });
}
}
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}

View file

@ -0,0 +1,33 @@
import { cloneDeep, omit } from 'lodash';
import { v4 as uuid } from 'uuid';
export function convertAppDefinitionFromSinglePageToMultiPage(appDefinition: any) {
const components = cloneDeep(appDefinition.components);
const newPageId = uuid();
const handle = 'home';
const name = 'Home';
const newAppDefinition = omit(
{
...appDefinition,
homePageId: newPageId,
pages: {
[newPageId]: { name, handle, components },
},
showViewerNavigation: false,
},
['components']
);
return newAppDefinition;
}
export function convertAppDefinitionFromMultiPageToSinglePage(appDefinition: any) {
const components = cloneDeep(Object.values(appDefinition?.pages ?? {})?.[0]?.['components'] ?? {});
const newAppDefinition = omit(
{
...appDefinition,
components,
},
['pages']
);
return newAppDefinition;
}

View file

@ -0,0 +1,18 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddPageIdToThreads1669280005881 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.addColumn(
'threads',
new TableColumn({
name: 'page_id',
type: 'varchar',
isNullable: true,
})
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('threads', 'page_id');
}
}

View file

@ -37,6 +37,7 @@
"joi": "^17.4.1",
"js-base64": "^3.7.2",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"module-from-string": "^3.3.0",
"nestjs-pino": "^1.4.0",
"nodemailer": "^6.6.3",

View file

@ -63,6 +63,7 @@
"joi": "^17.4.1",
"js-base64": "^3.7.2",
"jszip": "^3.10.1",
"lodash": "^4.17.21",
"module-from-string": "^3.3.0",
"nestjs-pino": "^1.4.0",
"nodemailer": "^6.6.3",

View file

@ -62,7 +62,13 @@ export class CommentController {
if (!ability.can('fetchComments', Comment)) {
throw new ForbiddenException('You do not have permissions to perform this action');
}
const comments = await this.commentService.getNotifications(appId, user.id, query.isResolved, query.appVersionsId);
const comments = await this.commentService.getNotifications(
appId,
user.id,
query.isResolved,
query.appVersionsId,
query.pageId
);
return comments;
}

View file

@ -21,6 +21,10 @@ export class CreateThreadDto {
@IsBoolean()
@IsOptional()
isResolved: boolean;
@IsUUID()
@IsOptional()
pageId: string;
}
export class UpdateThreadDto extends PartialType(CreateThreadDto) {}

View file

@ -30,6 +30,9 @@ export class Thread extends BaseEntity {
@Column({ default: false, name: 'is_resolved' })
isResolved: boolean;
@Column({ name: 'page_id' })
pageId: string;
@ManyToOne(() => User, (user) => user.id)
@JoinColumn({ name: 'user_id' })
user: User;

View file

@ -5,7 +5,7 @@ import { CreateThreadDto, UpdateThreadDto } from '../dto/thread.dto';
@EntityRepository(Thread)
export class ThreadRepository extends Repository<Thread> {
public async createThread(createThreadDto: CreateThreadDto, userId: string, organizationId: string): Promise<Thread> {
const { x, y, appId, appVersionsId } = createThreadDto;
const { x, y, appId, appVersionsId, pageId } = createThreadDto;
const thread = new Thread();
thread.x = x;
@ -14,6 +14,7 @@ export class ThreadRepository extends Repository<Thread> {
thread.userId = userId;
thread.organizationId = organizationId;
thread.appVersionsId = appVersionsId;
thread.pageId = pageId;
return await thread.save();
}

View file

@ -10,6 +10,7 @@ import { AppGroupPermission } from 'src/entities/app_group_permission.entity';
import { DataSourcesService } from './data_sources.service';
import { dbTransactionWrap } from 'src/helpers/utils.helper';
import { isEmpty } from 'lodash';
import { convertAppDefinitionFromSinglePageToMultiPage } from '../../lib/single-page-to-and-from-multipage-definition-conversion';
@Injectable()
export class AppImportExportService {
@ -59,6 +60,9 @@ export class AppImportExportService {
appToExport['dataQueries'] = await queryDataQueries.getMany();
appToExport['dataSources'] = await queryDataSources.getMany();
appToExport['appVersions'] = await queryAppVersions.getMany();
appToExport['schemaDetails'] = {
multiPages: true,
};
return appToExport;
}
@ -70,9 +74,13 @@ export class AppImportExportService {
let importedApp: App;
const schemaUnifiedAppParams = appParams?.schemaDetails?.multiPages
? appParams
: convertSinglePageSchemaToMultiPageSchema(appParams);
await dbTransactionWrap(async (manager) => {
importedApp = await this.createImportedAppForUser(manager, appParams, user);
await this.buildImportedAppAssociations(manager, importedApp, appParams);
importedApp = await this.createImportedAppForUser(manager, schemaUnifiedAppParams, user);
await this.buildImportedAppAssociations(manager, importedApp, schemaUnifiedAppParams);
await this.createAdminGroupPermissions(manager, importedApp);
});
@ -255,51 +263,66 @@ export class AppImportExportService {
}
replaceDataQueryIdWithinDefinitions(definition, dataQueryMapping) {
if (definition?.components) {
for (const id of Object.keys(definition.components)) {
const component = definition.components[id].component;
if (definition?.pages) {
for (const pageId of Object.keys(definition?.pages)) {
if (definition.pages[pageId].components) {
for (const id of Object.keys(definition.pages[pageId].components)) {
const component = definition.pages[pageId].components[id].component;
if (component?.definition?.events) {
const replacedComponentEvents = component.definition.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
}
return event;
});
component.definition.events = replacedComponentEvents;
}
if (component?.definition?.properties?.actions?.value) {
for (const value of component.definition.properties.actions.value) {
if (value?.events) {
const replacedComponentActionEvents = value.events.map((event) => {
if (component?.definition?.events) {
const replacedComponentEvents = component.definition.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
}
return event;
});
value.events = replacedComponentActionEvents;
component.definition.events = replacedComponentEvents;
}
}
}
if (component?.component === 'Table') {
for (const column of component?.definition?.properties?.columns?.value ?? []) {
if (column?.events) {
const replacedComponentActionEvents = column.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
if (component?.definition?.properties?.actions?.value) {
for (const value of component.definition.properties.actions.value) {
if (value?.events) {
const replacedComponentActionEvents = value.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
}
return event;
});
value.events = replacedComponentActionEvents;
}
return event;
});
column.events = replacedComponentActionEvents;
}
}
if (component?.component === 'Table') {
for (const column of component?.definition?.properties?.columns?.value ?? []) {
if (column?.events) {
const replacedComponentActionEvents = column.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
}
return event;
});
column.events = replacedComponentActionEvents;
}
}
}
definition.pages[pageId].components[id].component = component;
}
}
definition.components[id].component = component;
}
}
return definition;
}
}
function convertSinglePageSchemaToMultiPageSchema(appParams: any) {
const appParamsWithMultipageSchema = {
...appParams,
appVersions: appParams.appVersions?.map((appVersion) => ({
...appVersion,
definition: convertAppDefinitionFromSinglePageToMultiPage(appVersion.definition),
})),
};
return appParamsWithMultipageSchema;
}

View file

@ -391,49 +391,53 @@ export class AppsService {
}
replaceDataQueryIdWithinDefinitions(definition, dataQueryMapping) {
if (definition?.components) {
for (const id of Object.keys(definition.components)) {
const component = definition.components[id].component;
if (definition?.pages) {
for (const pageId of Object.keys(definition?.pages)) {
if (definition.pages[pageId].components) {
for (const id of Object.keys(definition.pages[pageId].components)) {
const component = definition.pages[pageId].components[id].component;
if (component?.definition?.events) {
const replacedComponentEvents = component.definition.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
}
return event;
});
component.definition.events = replacedComponentEvents;
}
if (component?.definition?.properties?.actions?.value) {
for (const value of component.definition.properties.actions.value) {
if (value?.events) {
const replacedComponentActionEvents = value.events.map((event) => {
if (component?.definition?.events) {
const replacedComponentEvents = component.definition.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
}
return event;
});
value.events = replacedComponentActionEvents;
component.definition.events = replacedComponentEvents;
}
}
}
if (component?.component === 'Table') {
for (const column of component?.definition?.properties?.columns?.value ?? []) {
if (column?.events) {
const replacedComponentActionEvents = column.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
if (component?.definition?.properties?.actions?.value) {
for (const value of component.definition.properties.actions.value) {
if (value?.events) {
const replacedComponentActionEvents = value.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
}
return event;
});
value.events = replacedComponentActionEvents;
}
return event;
});
column.events = replacedComponentActionEvents;
}
}
if (component?.component === 'Table') {
for (const column of component?.definition?.properties?.columns?.value ?? []) {
if (column?.events) {
const replacedComponentActionEvents = column.events.map((event) => {
if (event.queryId) {
event.queryId = dataQueryMapping[event.queryId];
}
return event;
});
column.events = replacedComponentActionEvents;
}
}
}
definition.pages[pageId].components[id].component = component;
}
}
definition.components[id].component = component;
}
}
return definition;

View file

@ -93,7 +93,8 @@ export class CommentService {
appId: string,
userId: string,
isResolved = false,
appVersionsId: string
appVersionsId: string,
pageId: string
): Promise<Comment[]> {
const comments = await createQueryBuilder(Comment, 'comment')
.innerJoin('comment.user', 'user')
@ -103,6 +104,9 @@ export class CommentService {
.andWhere('thread.appId = :appId', {
appId,
})
.andWhere('thread.pageId = :pageId', {
pageId,
})
.andWhere('thread.isResolved = :isResolved', {
isResolved,
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -773,7 +773,7 @@
},
"showBulkUpdateActions": {
"type": "toggle",
"displayName": "Show update buttons"
"displayName": "Show bulk update actions"
},
"showBulkSelector": {
"type": "toggle",
@ -2091,7 +2091,15 @@
},
"appId": "2c069ded-9922-4ef1-923d-d7b4433b766c",
"createdAt": "2021-12-15T21:54:07.261Z",
"updatedAt": "2022-01-19T13:12:04.860Z"
"updatedAt": "2022-01-19T13:12:04.860Z",
"homePageId": "3dcfcaf8-b273-4d36-bedf-c78033f05f6d",
"pages": {
"3dcfcaf8-b273-4d36-bedf-c78033f05f6d": {
"name": "Home",
"handle": "home"
}
},
"showViewerNavigation": false
}
],
"editingVersion": {
@ -2736,7 +2744,7 @@
},
"showBulkUpdateActions": {
"type": "toggle",
"displayName": "Show update buttons"
"displayName": "Show bulk update actions"
},
"showBulkSelector": {
"type": "toggle",
@ -4054,7 +4062,15 @@
},
"appId": "2c069ded-9922-4ef1-923d-d7b4433b766c",
"createdAt": "2021-12-15T21:54:07.261Z",
"updatedAt": "2022-01-19T13:12:04.860Z"
"updatedAt": "2022-01-19T13:12:04.860Z",
"homePageId": "954bb5f0-2808-4cb9-b168-0a6af854bff6",
"pages": {
"954bb5f0-2808-4cb9-b168-0a6af854bff6": {
"name": "Home",
"handle": "home"
}
},
"showViewerNavigation": false
},
"tooljetVersion": "0.13.5-ee1.3.6\n"
}

File diff suppressed because one or more lines are too long

View file

@ -574,7 +574,7 @@
},
"showBulkUpdateActions": {
"type": "toggle",
"displayName": "Show update buttons"
"displayName": "Show bulk update actions"
},
"showBulkSelector": {
"type": "toggle",
@ -1390,7 +1390,15 @@
},
"appId": "e115ddba-cf45-4246-91ee-6bb40101a12d",
"createdAt": "2021-12-16T15:52:12.610Z",
"updatedAt": "2021-12-16T16:21:24.728Z"
"updatedAt": "2021-12-16T16:21:24.728Z",
"homePageId": "77ca5170-7220-44a9-9c8e-0e203f42850f",
"pages": {
"77ca5170-7220-44a9-9c8e-0e203f42850f": {
"name": "Home",
"handle": "home"
}
},
"showViewerNavigation": false
}
],
"editingVersion": {
@ -1868,7 +1876,7 @@
},
"showBulkUpdateActions": {
"type": "toggle",
"displayName": "Show update buttons"
"displayName": "Show bulk update actions"
},
"showBulkSelector": {
"type": "toggle",
@ -2684,7 +2692,15 @@
},
"appId": "e115ddba-cf45-4246-91ee-6bb40101a12d",
"createdAt": "2021-12-16T15:52:12.610Z",
"updatedAt": "2021-12-16T16:21:24.728Z"
"updatedAt": "2021-12-16T16:21:24.728Z",
"homePageId": "1c368412-aad1-40cf-a0b9-eadba9e798d6",
"pages": {
"1c368412-aad1-40cf-a0b9-eadba9e798d6": {
"name": "Home",
"handle": "home"
}
},
"showViewerNavigation": false
},
"tooljetVersion": "0.11.0"
}

View file

@ -0,0 +1,24 @@
const fs = require('fs');
const convertAppDefinitionFromSinglePageToMultiPage = require('../lib/single-page-to-and-from-multipage-definition-conversion.ts')
fs.readdir('./', function (err, files) {
//handling error
if (err) {
return console.log('Unable to scan directory: ' + err);
}
//listing all files using forEach
for (const file of files) {
fs.readFile(file + '/definition.json', 'utf8', (err, data) => {
if (!err) {
const appData = JSON.parse(data);
const newAppData = {
...appData,
//appVersions: appData.appVersions?.map(convertAppDefinitionFromSinglePageToMultiPage),
editingVersion: convertAppDefinitionFromSinglePageToMultiPage(appData.editingVersion)
};
console.log({ newAppData });
}
})
}
});

View file

@ -753,7 +753,7 @@
},
"showBulkUpdateActions": {
"type": "toggle",
"displayName": "Show update buttons"
"displayName": "Show bulk update actions"
},
"showBulkSelector": {
"type": "toggle",
@ -1537,7 +1537,15 @@
},
"appId": "05dda983-aa25-4a4c-b52d-bf703eec2658",
"createdAt": "2021-12-28T19:18:12.890Z",
"updatedAt": "2021-12-30T05:05:39.802Z"
"updatedAt": "2021-12-30T05:05:39.802Z",
"homePageId": "187cbb5d-d19d-4c3f-aa91-dc440f00a7d0",
"pages": {
"187cbb5d-d19d-4c3f-aa91-dc440f00a7d0": {
"name": "Home",
"handle": "home"
}
},
"showViewerNavigation": false
}
],
"editingVersion": {
@ -1935,7 +1943,7 @@
},
"showBulkUpdateActions": {
"type": "toggle",
"displayName": "Show update buttons"
"displayName": "Show bulk update actions"
},
"showBulkSelector": {
"type": "toggle",
@ -2719,7 +2727,15 @@
},
"appId": "05dda983-aa25-4a4c-b52d-bf703eec2658",
"createdAt": "2021-12-28T19:18:12.890Z",
"updatedAt": "2021-12-30T05:05:39.802Z"
"updatedAt": "2021-12-30T05:05:39.802Z",
"homePageId": "6c280ec2-a6b9-487e-90cf-f467e1349e11",
"pages": {
"6c280ec2-a6b9-487e-90cf-f467e1349e11": {
"name": "Home",
"handle": "home"
}
},
"showViewerNavigation": false
},
"tooljetVersion": "0.13.5-ee1.3.6\n"
}

View file

@ -52,7 +52,7 @@ describe('thread controller', () => {
expect(response.statusCode).toBe(200);
expect(response.body).toHaveLength(1);
expect(Object.keys(response.body[0]).sort()).toEqual(
['id', 'x', 'y', 'appId', 'appVersionsId', 'userId', 'organizationId', 'isResolved', 'user'].sort()
['id', 'x', 'y', 'appId', 'appVersionsId', 'userId', 'organizationId', 'isResolved', 'user', 'pageId'].sort()
);
});

View file

@ -492,6 +492,7 @@ export async function createThread(_nestApp, { appId, x, y, userId, organization
isResolved: false,
organizationId,
appVersionsId,
pageId: 'placeholder',
},
userId,
organizationId