├── .gitignore ├── LICENSE ├── README.md ├── lowdefy.yaml ├── pages ├── new-ticket │ ├── new-ticket.yaml │ └── requests │ │ └── insert_ticket.yaml └── tickets │ ├── actions │ ├── fetch_selected.yaml │ ├── fetch_tickets.yaml │ └── set_flagged.yaml │ ├── components │ ├── change_status_modal.yaml │ ├── comment_modal.yaml │ ├── confirm_close_modal.yaml │ ├── escalate_modal.yaml │ ├── search.yaml │ ├── search_results.yaml │ └── selected.yaml │ ├── logic │ ├── no_selected.yaml │ ├── no_tickets.yaml │ ├── selected_loading.yaml │ ├── ticket_selected.yaml │ └── tickets_loading.yaml │ ├── requests │ ├── change_status_on_ticket.yaml │ ├── close_ticket.yaml │ ├── comment_on_ticket.yaml │ ├── escalate_ticket.yaml │ ├── fetch_selected.yaml │ ├── fetch_tickets.yaml │ └── set_flagged.yaml │ └── tickets.yaml └── shared └── components └── view_github.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | .lowdefy/** 2 | .env -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Lowdefy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎫 Lowdefy Case Management Example 2 | 3 | > [View this example.](https://example-case-management.lowdefy.com) 4 | 5 | This example focuses on building a rich UI for a hypothetical case management app, in a customer relations setting. 6 | 7 | It provides a interface where cases can be searched and filtered. Cases have a status that can be changed, and can be commented on, flagged, escalated, or closed. 8 | 9 | The main purpose of this example is to show what types of apps can be built with Lowdefy, how to construct rich UIs, and how the MongoDB connection can be used. 10 | 11 | ## ⚙️ Running this example 12 | 13 | - Create a MongoDB cluster and get a URI connection string: 14 | - Create a free MongoDB database cluster hosted by [MongoDB Atlas](https://www.mongodb.com/try). 15 | - Create a new database called `example-case-management`. 16 | - Add a new collection to this database called `tickets`. 17 | - In the Database access section, create a database user with read and write access to any database (You can also specify the database as `example-case-management`). 18 | - In the main cluster view, click "connect", then "Connect you application". This will give a MongoDB URI connection string. Use the credentials you just created. 19 | - You can read more about the [Lowdefy MongoDB connector](https://docs.lowdefy.com/MongoDB). 20 | - Clone this repository. 21 | - Create a `.env` file in your project folder and set your MongoDB database connector URI as a variable in the `.env` file: `LOWDEFY_SECRET_EXAMPLES_MDB="{{ your_mongodb_connection_uri }}"` 22 | - In the command console, navigate to your project folder and run the Lowdefy CLI: `pnpx lowdefy@4 dev`. 23 | 24 | ## 🔗 More Lowdefy resources 25 | 26 | - Getting started with Lowdefy - https://docs.lowdefy.com/tutorial-start 27 | - Lowdefy docs - https://docs.lowdefy.com 28 | - Lowdefy website - https://lowdefy.com 29 | - Community forum - https://github.com/lowdefy/lowdefy/discussions 30 | - Bug reports and feature requests - https://github.com/lowdefy/lowdefy/issues 31 | - Discord - https://discord.gg/WmcJgXt 32 | 33 | ## ⚖️ Licence 34 | 35 | [MIT](https://github.com/lowdefy/lowdefy-example-case-management/blob/main/LICENSE) 36 | -------------------------------------------------------------------------------- /lowdefy.yaml: -------------------------------------------------------------------------------- 1 | name: lowdefy-example-case-management 2 | lowdefy: 4.0.1 3 | license: MIT 4 | 5 | connections: 6 | - id: tickets 7 | type: MongoDBCollection 8 | properties: 9 | databaseUri: 10 | _secret: EXAMPLES_MDB 11 | databaseName: example-case-management 12 | collection: tickets 13 | write: true 14 | 15 | menus: 16 | - id: default 17 | links: 18 | - id: tickets 19 | type: MenuLink 20 | pageId: tickets 21 | properties: 22 | icon: AiOutlineProfile 23 | title: Tickets 24 | - id: new-ticket 25 | type: MenuLink 26 | pageId: new-ticket 27 | properties: 28 | icon: AiOutlineAlert 29 | title: New Ticket 30 | 31 | global: 32 | statuses: 33 | - Investigation Started 34 | - Client Contacted 35 | - Awaiting Confirmation 36 | 37 | # Maps of colors and icons for statuses and actions 38 | statusColors: 39 | New: "#722ed1" 40 | Investigation Started: "#1890ff" 41 | Client Contacted: "#13c2c2" 42 | Awaiting Confirmation: "#7cb305" 43 | Escalated: "#faad14" 44 | Closed: "#bfbfbf" 45 | 46 | actionColors: 47 | Created: "#722ed1" 48 | Changed status to Investigation Started: "#1890ff" 49 | Changed status to Client Contacted: "#13c2c2" 50 | Changed status to Awaiting Confirmation: "#7cb305" 51 | Escalated: "#faad14" 52 | Flagged: "#f5222d" 53 | Removed flag: "#bfbfbf" 54 | Commented: "#bfbfbf" 55 | Closed: "#52c41a" 56 | 57 | statusIcons: 58 | New: AiOutlinePlusCircle 59 | Escalated: AiOutlineExclamationCircle 60 | Investigation Started: AiOutlineTool 61 | Client Contacted: AiOutlineSound 62 | Awaiting Confirmation: AiOutlineLike 63 | Closed: AiOutlineStop 64 | 65 | actionIcons: 66 | Created: AiOutlinePlusCircle 67 | Changed status to Investigation Started: AiOutlineTool 68 | Changed status to Client Contacted: AiOutlineSound 69 | Changed status to Awaiting Confirmation: AiOutlineLike 70 | Escalated: AiOutlineExclamationCircle 71 | Flagged: AiOutlineFlag 72 | Removed flag: AiOutlineFlag 73 | Commented: AiOutlineMessage 74 | Closed: AiOutlineStop 75 | 76 | pages: 77 | - _ref: pages/tickets/tickets.yaml 78 | - _ref: pages/new-ticket/new-ticket.yaml 79 | -------------------------------------------------------------------------------- /pages/new-ticket/new-ticket.yaml: -------------------------------------------------------------------------------- 1 | id: new-ticket 2 | type: PageHeaderMenu 3 | properties: 4 | title: New Ticket 5 | content: 6 | style: 7 | background: '#f5f5f5' 8 | 9 | requests: 10 | - _ref: pages/new-ticket/requests/insert_ticket.yaml 11 | 12 | areas: 13 | header: 14 | blocks: 15 | - _ref: shared/components/view_github.yaml 16 | 17 | content: 18 | justify: center 19 | blocks: 20 | # This box limits the width of the content to 800px 21 | - id: max_width 22 | type: Box 23 | layout: 24 | size: 800px # Initial width of the box 25 | shrink: 1 # Allow the box to shrink if available space on screen is less than 800px 26 | contentGutter: 8 27 | blocks: 28 | - id: ticket_title 29 | type: Title 30 | properties: 31 | content: New query 32 | level: 2 33 | - id: name 34 | type: TextInput 35 | required: true 36 | properties: 37 | title: Name 38 | size: large 39 | - id: company 40 | type: AutoComplete 41 | required: true 42 | properties: 43 | title: Company 44 | size: large 45 | options: 46 | - Company, Inc. 47 | - Distribution Logistics 48 | - GHT Consulting 49 | - id: phone 50 | type: TextInput 51 | required: true 52 | properties: 53 | title: Phone 54 | size: large 55 | - id: type 56 | type: Selector 57 | required: true 58 | properties: 59 | title: Ticket type 60 | size: large 61 | options: 62 | - Invoices 63 | - Late Delivery 64 | - Return Deposit 65 | - Other 66 | - id: ticket_description 67 | type: TextArea 68 | required: true 69 | properties: 70 | title: Ticket description 71 | size: large 72 | - id: submit 73 | type: Button 74 | style: 75 | marginTop: 8 76 | properties: 77 | title: Submit 78 | size: large 79 | block: true 80 | events: 81 | # Validate inputs, insert the ticket and reset when the submit button is clicked 82 | onClick: 83 | - id: validate 84 | type: Validate 85 | - id: submit 86 | type: Request 87 | params: insert_ticket 88 | - id: reset 89 | type: Reset 90 | -------------------------------------------------------------------------------- /pages/new-ticket/requests/insert_ticket.yaml: -------------------------------------------------------------------------------- 1 | # insert new ticket to MongoDB 2 | id: insert_ticket 3 | type: MongoDBInsertOne 4 | connectionId: tickets 5 | payload: 6 | state: 7 | _state: true 8 | properties: 9 | doc: 10 | # Use _object.assign to create a new object with all the values from state, and some extra fields 11 | _object.assign: 12 | - _payload: state 13 | - created_date: 14 | _date: now 15 | updated_date: 16 | _date: now 17 | status: New 18 | # This is an array of all the actions that have been taken with the ticket 19 | history: 20 | - action: Created 21 | status: New 22 | timestamp: 23 | _date: now 24 | # These are randomly generated numbers to look pretty in the UI on the tickets page. 25 | # These might have been looked up from other data regarding the customer. 26 | satisfaction_score: 27 | _random: 28 | type: integer 29 | max: 100 30 | min: 20 31 | past_orders: 32 | _random: 33 | type: integer 34 | max: 40 35 | min: 0 36 | ticket_id: 37 | _random: 38 | type: integer 39 | max: 999999 40 | min: 100000 41 | -------------------------------------------------------------------------------- /pages/tickets/actions/fetch_selected.yaml: -------------------------------------------------------------------------------- 1 | - id: prepare_fetch_selected 2 | type: SetState 3 | params: 4 | # Set a flag in state to indicate the selected item is being loaded 5 | selected_loading: true 6 | # Clear the currently selected item 7 | selected: {} 8 | # Set the selected id that will be used by the fetch_selected request 9 | selected_id: 10 | _state: tickets_list.$._id 11 | - id: fetch_selected 12 | type: Request 13 | params: 14 | - fetch_selected 15 | - id: finalize_fetch_selected 16 | type: SetState 17 | params: 18 | # Set the loading flag to indicate loading has completed 19 | selected_loading: false 20 | # Set the selected ticket value to state 21 | # This is so that ticket fields can be accessed using the _state operator 22 | selected: 23 | _request: fetch_selected.0 24 | -------------------------------------------------------------------------------- /pages/tickets/actions/fetch_tickets.yaml: -------------------------------------------------------------------------------- 1 | - id: prepare_fetch_tickets 2 | type: SetState 3 | params: 4 | # Set a flag in state to indicate the tickets are loading 5 | tickets_loading: true 6 | - id: fetch 7 | type: Request 8 | params: 9 | - fetch_tickets 10 | - id: finalize_fetch_tickets 11 | type: SetState 12 | params: 13 | # Set the loading flag to indicate loading has completed 14 | tickets_loading: false 15 | # Set the tickets in state to populate the tickets list 16 | tickets_list: 17 | _request: fetch_tickets -------------------------------------------------------------------------------- /pages/tickets/actions/set_flagged.yaml: -------------------------------------------------------------------------------- 1 | - id: set_flagged 2 | type: Request 3 | params: 4 | - set_flagged 5 | - id: refresh 6 | type: Request 7 | params: 8 | - fetch_selected 9 | - fetch_tickets 10 | - id: update_state 11 | type: SetState 12 | params: 13 | selected: 14 | _request: fetch_selected.0 15 | tickets_list: 16 | _request: fetch_tickets 17 | -------------------------------------------------------------------------------- /pages/tickets/components/change_status_modal.yaml: -------------------------------------------------------------------------------- 1 | id: selected.change_status_modal 2 | type: Modal 3 | properties: 4 | okText: Change Status 5 | events: 6 | onOk: 7 | - id: validate 8 | type: Validate 9 | messages: 10 | error: A status is required. 11 | params: selected.change_status_selector 12 | - id: change_status_on_ticket 13 | type: Request 14 | params: change_status_on_ticket 15 | - id: refresh 16 | type: Request 17 | params: 18 | - fetch_tickets 19 | - fetch_selected 20 | - id: set_state 21 | type: SetState 22 | params: 23 | selected: 24 | _request: fetch_selected.0 25 | tickets_list: 26 | _request: fetch_tickets 27 | blocks: 28 | - id: selected.change_status_title 29 | type: Markdown 30 | style: 31 | marginBottom: 16px 32 | properties: 33 | content: '#### Change Status' 34 | - id: selected.change_status_selector 35 | type: Selector 36 | required: true 37 | properties: 38 | title: New Status 39 | options: 40 | _global: statuses 41 | - id: selected.change_status_comment 42 | type: TextArea 43 | properties: 44 | title: comment 45 | -------------------------------------------------------------------------------- /pages/tickets/components/comment_modal.yaml: -------------------------------------------------------------------------------- 1 | id: selected.comment_modal 2 | type: Modal 3 | properties: 4 | okText: Save 5 | events: 6 | onOk: 7 | - id: validate 8 | type: Validate 9 | messages: 10 | error: A comment is required. 11 | params: selected.comment_input 12 | - id: comment_on_ticket 13 | type: Request 14 | params: comment_on_ticket 15 | - id: refresh 16 | type: Request 17 | params: 18 | - fetch_tickets 19 | - fetch_selected 20 | - id: set_state 21 | type: SetState 22 | params: 23 | selected: 24 | _request: fetch_selected.0 25 | tickets_list: 26 | _request: fetch_tickets 27 | blocks: 28 | - id: selected.comment_modal_title 29 | type: Markdown 30 | style: 31 | marginBottom: 16px 32 | properties: 33 | content: '#### Comment' 34 | - id: selected.comment_input 35 | type: TextArea 36 | required: true 37 | properties: 38 | label: 39 | disabled: true 40 | -------------------------------------------------------------------------------- /pages/tickets/components/confirm_close_modal.yaml: -------------------------------------------------------------------------------- 1 | id: selected.confirm_close_modal 2 | type: Modal 3 | properties: 4 | okButtonProps: 5 | danger: true 6 | okText: Close 7 | events: 8 | onOk: 9 | - id: close_ticket 10 | type: Request 11 | params: close_ticket 12 | - id: reset_selected 13 | type: SetState 14 | params: 15 | selected_id: null 16 | selected: {} 17 | - id: refresh 18 | type: Request 19 | params: 20 | - fetch_tickets 21 | - id: set_tickets 22 | type: SetState 23 | params: 24 | tickets_list: 25 | _request: fetch_tickets 26 | blocks: 27 | - id: selected.confirm_close_modal_text 28 | type: Markdown 29 | properties: 30 | content: '#### Are you sure you want to close this issue?' 31 | -------------------------------------------------------------------------------- /pages/tickets/components/escalate_modal.yaml: -------------------------------------------------------------------------------- 1 | id: selected.escalate_modal 2 | type: Modal 3 | properties: 4 | okText: Escalate 5 | events: 6 | onOk: 7 | - id: validate 8 | type: Validate 9 | messages: 10 | error: A comment is required. 11 | params: selected.escalate_comment 12 | - id: escalate_ticket 13 | type: Request 14 | params: escalate_ticket 15 | - id: refresh 16 | type: Request 17 | params: 18 | - fetch_tickets 19 | - fetch_selected 20 | - id: set_state 21 | type: SetState 22 | params: 23 | selected: 24 | _request: fetch_selected.0 25 | tickets_list: 26 | _request: fetch_tickets 27 | blocks: 28 | - id: selected.comment_modal_title 29 | type: Markdown 30 | style: 31 | marginBottom: 16px 32 | properties: 33 | content: '#### Escalate ticket' 34 | - id: selected.escalate_comment 35 | type: TextArea 36 | required: true 37 | properties: 38 | title: comment 39 | -------------------------------------------------------------------------------- /pages/tickets/components/search.yaml: -------------------------------------------------------------------------------- 1 | id: search_box 2 | type: Box 3 | layout: 4 | contentGutter: 6 5 | blocks: 6 | - id: search_input_box 7 | type: Box 8 | layout: 9 | contentGutter: 6 10 | blocks: 11 | - id: search_input 12 | type: TextInput 13 | layout: 14 | flex: 1 1 auto 15 | properties: 16 | disabled: 17 | # Logical tests that are used often are also referenced 18 | # The inputs should be disabled when the tickets loading, so the user can only make one request at a time 19 | # A loading state is set by the fetch_tickets action, that is used to determine if he tickets are loading 20 | _ref: pages/tickets/logic/tickets_loading.yaml 21 | placeholder: Search 22 | label: 23 | disabled: true 24 | events: 25 | onPressEnter: 26 | # Fetch the tickets when enter is pressed and the text input is focused 27 | _ref: pages/tickets/actions/fetch_tickets.yaml 28 | - id: search_button 29 | type: Button 30 | layout: 31 | flex: 0 1 auto 32 | properties: 33 | disabled: 34 | _ref: pages/tickets/logic/tickets_loading.yaml 35 | title: Search 36 | icon: AiOutlineSearch 37 | events: 38 | onClick: 39 | _ref: pages/tickets/actions/fetch_tickets.yaml 40 | - id: status_selector 41 | type: Selector 42 | layout: 43 | span: 6 44 | properties: 45 | disabled: 46 | _ref: pages/tickets/logic/tickets_loading.yaml 47 | label: 48 | disabled: true 49 | placeholder: Status 50 | options: 51 | _global: statuses 52 | size: small 53 | events: 54 | onChange: 55 | _ref: pages/tickets/actions/fetch_tickets.yaml 56 | - id: type_selector 57 | type: Selector 58 | layout: 59 | span: 6 60 | properties: 61 | disabled: 62 | _ref: pages/tickets/logic/tickets_loading.yaml 63 | label: 64 | disabled: true 65 | placeholder: Type 66 | options: 67 | - Invoices 68 | - Late Delivery 69 | - Return Deposit 70 | - Other 71 | size: small 72 | events: 73 | onChange: 74 | _ref: pages/tickets/actions/fetch_tickets.yaml 75 | - id: open_closed_selector 76 | type: Selector 77 | layout: 78 | span: 6 79 | properties: 80 | allowClear: false 81 | disabled: 82 | _ref: pages/tickets/logic/tickets_loading.yaml 83 | label: 84 | disabled: true 85 | options: 86 | - Open 87 | - Closed 88 | size: small 89 | events: 90 | onChange: 91 | _ref: pages/tickets/actions/fetch_tickets.yaml 92 | - id: flagged_selector 93 | type: CheckboxSelector 94 | layout: 95 | span: 6 96 | properties: 97 | disabled: 98 | _ref: pages/tickets/logic/tickets_loading.yaml 99 | label: 100 | disabled: true 101 | options: 102 | - Only flagged 103 | events: 104 | onChange: 105 | _ref: pages/tickets/actions/fetch_tickets.yaml 106 | -------------------------------------------------------------------------------- /pages/tickets/components/search_results.yaml: -------------------------------------------------------------------------------- 1 | id: results_card 2 | type: Card 3 | # This Skeleton is shown when the tickets are loading 4 | loading: 5 | _ref: pages/tickets/logic/tickets_loading.yaml 6 | skeleton: 7 | type: Card 8 | blocks: 9 | - type: Skeleton 10 | properties: 11 | height: 220 12 | style: 13 | marginTop: 4 14 | blocks: 15 | # This is shown when the tickets request does not return any tickets 16 | - id: no_tickets 17 | type: Result 18 | visible: 19 | _ref: pages/tickets/logic/no_tickets.yaml 20 | properties: 21 | icon: 22 | name: AiOutlineContainer 23 | color: '#bfbfbf' 24 | subTitle: No tickets found 25 | # A list to display the list of tickets 26 | - id: tickets_list 27 | type: List 28 | visible: 29 | _not: 30 | _ref: pages/tickets/logic/tickets_loading.yaml 31 | blocks: 32 | # This box is here so we can add a onClick event if the user clicks anywhere on the ticket 33 | # All ids of blocks that are part of the tickets list start with "tickets_list.$." 34 | - id: tickets_list.$.box 35 | type: Box 36 | events: 37 | onClick: 38 | _ref: pages/tickets/actions/fetch_selected.yaml 39 | blocks: 40 | # A box to contain the header line of the ticket display 41 | # The items inside are laid out using flex properties 42 | # The status icon, status text and flagged icon all have grow 0, shrink 1 and size auto 43 | # They will try to occupy space they size they naturally are, and shrink if possible. 44 | # The ticket id has grow 1, shrink 1 and size auto. 45 | # It will expand to take up as much space as possible , pushing the flag icon to the right 46 | # This pattern is useful when creating a row of objects that should be aligned 47 | # to both the lhs and rhs of the row 48 | - id: tickets_list.$.header_box 49 | type: Box 50 | layout: 51 | contentAlign: middle 52 | contentGutter: 16 53 | blocks: 54 | - id: tickets_list.$.status_icon 55 | type: Icon 56 | layout: 57 | flex: 0 1 auto 58 | properties: 59 | size: 12 60 | color: 61 | # Use the _get operator to get the correct icon name and color from the global maps 62 | _get: 63 | from: 64 | _global: statusColors 65 | key: 66 | _state: tickets_list.$.status 67 | name: 68 | _get: 69 | from: 70 | _global: statusIcons 71 | key: 72 | _state: tickets_list.$.status 73 | - id: tickets_list.$.status 74 | type: Html 75 | layout: 76 | flex: 0 1 auto 77 | properties: 78 | html: 79 | _nunjucks: 80 | template: | 81 | 82 | {{ status }} 83 | 84 | on: 85 | _state: tickets_list.$ 86 | - id: tickets_list.$.ticket_id 87 | type: Html 88 | layout: 89 | flex: 1 1 auto 90 | properties: 91 | html: 92 | _nunjucks: 93 | template: | 94 | 95 | #{{ ticket_id }} 96 | 97 | on: 98 | _state: tickets_list.$ 99 | - id: tickets_list.$.flag 100 | type: Icon 101 | layout: 102 | flex: 0 1 auto 103 | properties: 104 | size: 12 105 | name: AiOutlineFlag 106 | color: 107 | _if: 108 | test: 109 | _eq: 110 | - _state: tickets_list.$.flagged 111 | - true 112 | then: '#f5222d' 113 | else: '#bfbfbf' 114 | - id: tickets_list.$.description 115 | type: Html 116 | properties: 117 | html: 118 | _nunjucks: 119 | template: | 120 |

121 | 122 | {{ company }} 123 | 124 | 125 | {{ name }} 126 | 127 |

128 | Created: {{ created_date | date("lll") }} {{ created_user.name }} 129 | on: 130 | _state: tickets_list.$ 131 | - id: tickets_list.$.divider 132 | type: Divider 133 | visible: 134 | # Hide the divider if the item is the last in the list 135 | _not: 136 | _eq: 137 | - _sum: 138 | - _index: 0 139 | - 1 140 | - _array.length: 141 | _state: tickets_list 142 | -------------------------------------------------------------------------------- /pages/tickets/components/selected.yaml: -------------------------------------------------------------------------------- 1 | # This box contains the ticket description on the rhs of the screen. 2 | id: selected_box 3 | type: Box 4 | layout: 5 | span: 14 6 | blocks: 7 | - id: selected.title 8 | type: Markdown 9 | loading: 10 | _ref: pages/tickets/logic/selected_loading.yaml 11 | skeleton: 12 | type: SkeletonParagraph 13 | properties: 14 | lines: 1 15 | properties: 16 | content: '###### Selected' 17 | - id: selected.card 18 | type: Card 19 | loading: 20 | _ref: pages/tickets/logic/selected_loading.yaml 21 | skeleton: 22 | type: Card 23 | blocks: 24 | - type: Skeleton 25 | properties: 26 | height: 220 27 | style: 28 | marginTop: 4 29 | blocks: 30 | - id: selected.not_selected 31 | type: Result 32 | visible: 33 | _ref: pages/tickets/logic/no_selected.yaml 34 | properties: 35 | icon: 36 | name: AiOutlineContainer 37 | color: '#bfbfbf' 38 | subTitle: No ticket selected 39 | - id: selected.visible_box 40 | type: Box 41 | visible: 42 | _ref: pages/tickets/logic/ticket_selected.yaml 43 | layout: 44 | contentGutter: 16 45 | blocks: 46 | - id: selected.header_box 47 | type: Box 48 | layout: 49 | contentAlign: middle 50 | contentGutter: 16 51 | blocks: 52 | - id: selected.status_icon 53 | type: Icon 54 | layout: 55 | flex: 0 1 auto 56 | properties: 57 | size: 32 58 | color: 59 | _get: 60 | from: 61 | _global: statusColors 62 | key: 63 | _state: selected.status 64 | name: 65 | _get: 66 | from: 67 | _global: statusIcons 68 | key: 69 | _state: selected.status 70 | - id: selected.status 71 | type: Html 72 | layout: 73 | flex: 0 1 auto 74 | properties: 75 | html: 76 | _nunjucks: 77 | template: | 78 | 79 | {{ status }} 80 | 81 | on: 82 | _state: selected 83 | - id: selected.ticket_id 84 | type: Html 85 | layout: 86 | flex: 1 1 auto 87 | properties: 88 | html: 89 | _nunjucks: 90 | template: | 91 | 92 | #{{ ticket_id }} 93 | 94 | on: 95 | _state: selected 96 | - id: selected.flag 97 | type: Icon 98 | layout: 99 | flex: 0 1 auto 100 | properties: 101 | size: 32 102 | name: AiOutlineFlag 103 | color: 104 | _if: 105 | test: 106 | _eq: 107 | - _state: selected.flagged 108 | - true 109 | then: '#f5222d' 110 | else: '#bfbfbf' 111 | events: 112 | onClick: 113 | _ref: pages/tickets/actions/set_flagged.yaml 114 | - id: selected.actions_box 115 | type: Box 116 | layout: 117 | contentGutter: 8 118 | blocks: 119 | - id: selected.change_status_button 120 | type: Button 121 | layout: 122 | flex: 1 1 auto 123 | properties: 124 | title: Change Status 125 | block: true 126 | type: default 127 | icon: AiOutlineRetweet 128 | events: 129 | onClick: 130 | - id: open_change_status_modal 131 | type: CallMethod 132 | params: 133 | blockId: selected.change_status_modal 134 | method: toggleOpen 135 | - id: selected.comment_button 136 | type: Button 137 | layout: 138 | flex: 1 1 auto 139 | properties: 140 | title: Comment 141 | block: true 142 | type: default 143 | icon: AiOutlineMessage 144 | events: 145 | onClick: 146 | - id: open_comment_modal 147 | type: CallMethod 148 | params: 149 | blockId: selected.comment_modal 150 | method: toggleOpen 151 | - id: selected.escalate_button 152 | type: Button 153 | layout: 154 | flex: 1 1 auto 155 | properties: 156 | title: Escalate 157 | block: true 158 | type: default 159 | icon: AiOutlineExclamationCircle 160 | events: 161 | onClick: 162 | - id: open_escalate_modal 163 | type: CallMethod 164 | params: 165 | blockId: selected.escalate_modal 166 | method: toggleOpen 167 | - id: selected.close_button 168 | type: Button 169 | layout: 170 | flex: 1 1 auto 171 | properties: 172 | disabled: 173 | _eq: 174 | - _state: selected.status 175 | - Closed 176 | title: Close 177 | block: true 178 | type: default 179 | icon: AiOutlineStop 180 | events: 181 | onClick: 182 | - id: open_confirm_close_modal 183 | type: CallMethod 184 | params: 185 | blockId: selected.confirm_close_modal 186 | method: toggleOpen 187 | - id: selected.description_box 188 | type: Box 189 | layout: 190 | contentGutter: 42 191 | blocks: 192 | - id: selected.descriptions 193 | type: Descriptions 194 | layout: 195 | span: 12 196 | properties: 197 | bordered: true 198 | column: 1 199 | size: small 200 | items: 201 | - label: Name 202 | value: 203 | _state: selected.name 204 | - label: Company 205 | value: 206 | _state: selected.company 207 | - label: Phone 208 | value: 209 | _state: selected.phone 210 | # These values are created randomly when the ticket is created 211 | # The could have been looked up from some customer data though 212 | - id: selected.satisfaction_box 213 | type: Box 214 | layout: 215 | span: 12 216 | contentJustify: center 217 | contentGutter: 4 218 | blocks: 219 | - id: selected.satisfaction_title 220 | type: Markdown 221 | properties: 222 | content: '###### Satisfaction score' 223 | - id: selected.satisfaction_score 224 | type: Progress 225 | properties: 226 | showInfo: true 227 | percent: 228 | _state: selected.satisfaction_score 229 | strokeColor: 230 | from: '#faad14' 231 | to: '#52c41a' 232 | - id: selected.satisfaction_title 233 | type: Markdown 234 | properties: 235 | content: '###### Past orders' 236 | - id: selected.past_orders 237 | type: Statistic 238 | properties: 239 | suffixIcon: AiOutlineShoppingCart 240 | value: 241 | _state: selected.past_orders 242 | - id: selected.ticket_description 243 | type: Markdown 244 | style: 245 | '.markdown-body': 246 | fontSize: 14 247 | properties: 248 | content: 249 | _nunjucks: 250 | template: | 251 | {{ ticket_description }} 252 | on: 253 | _state: selected 254 | 255 | - id: selected.history 256 | type: TimelineList 257 | properties: 258 | data: 259 | _mql.aggregate: 260 | pipeline: 261 | - $addFields: 262 | icons: 263 | $objectToArray: 264 | _global: actionIcons 265 | colors: 266 | $objectToArray: 267 | _global: actionColors 268 | - $project: 269 | icon: 270 | name: 271 | $arrayElemAt: 272 | - $icons.v 273 | - $indexOfArray: 274 | - $icons.k 275 | - $action 276 | color: 277 | $arrayElemAt: 278 | - $colors.v 279 | - $indexOfArray: 280 | - $colors.k 281 | - $action 282 | on: 283 | _state: selected.history 284 | 285 | blocks: 286 | - id: selected.history.$.description 287 | type: Html 288 | style: 289 | fontSize: 12 290 | properties: 291 | html: 292 | _nunjucks: 293 | template: | 294 |

{{ action }}

295 |

{{ timestamp | date("lll") }}

296 |

{{ comment }}

297 | on: 298 | _state: selected.history.$ 299 | -------------------------------------------------------------------------------- /pages/tickets/logic/no_selected.yaml: -------------------------------------------------------------------------------- 1 | _and: 2 | - _not: 3 | _ref: pages/tickets/logic/ticket_selected.yaml 4 | - _not: 5 | _ref: pages/tickets/logic/selected_loading.yaml 6 | -------------------------------------------------------------------------------- /pages/tickets/logic/no_tickets.yaml: -------------------------------------------------------------------------------- 1 | _and: 2 | - _eq: 3 | - _array.length: 4 | _if_none: 5 | - _state: tickets_list 6 | - [] 7 | - 0 8 | - _not: 9 | _ref: pages/tickets/logic/tickets_loading.yaml 10 | -------------------------------------------------------------------------------- /pages/tickets/logic/selected_loading.yaml: -------------------------------------------------------------------------------- 1 | _eq: 2 | - _state: selected_loading 3 | - true -------------------------------------------------------------------------------- /pages/tickets/logic/ticket_selected.yaml: -------------------------------------------------------------------------------- 1 | _not: 2 | _eq: 3 | - _state: selected._id 4 | - null -------------------------------------------------------------------------------- /pages/tickets/logic/tickets_loading.yaml: -------------------------------------------------------------------------------- 1 | _eq: 2 | - _state: tickets_loading 3 | - true -------------------------------------------------------------------------------- /pages/tickets/requests/change_status_on_ticket.yaml: -------------------------------------------------------------------------------- 1 | id: change_status_on_ticket 2 | type: MongoDBUpdateOne 3 | connectionId: tickets 4 | payload: 5 | id: 6 | _state: selected_id 7 | status: 8 | _state: selected.change_status_selector 9 | comment: 10 | _state: selected.change_status_comment 11 | properties: 12 | filter: 13 | _id: 14 | _payload: id 15 | update: 16 | $set: 17 | updated_date: 18 | _date: now 19 | status: 20 | _payload: status 21 | $push: 22 | history: 23 | action: 24 | _nunjucks: 25 | on: 26 | status: 27 | _payload: status 28 | template: 'Changed status to {{ status }}' 29 | status: 30 | _payload: status 31 | comment: 32 | _payload: comment 33 | timestamp: 34 | _date: now 35 | -------------------------------------------------------------------------------- /pages/tickets/requests/close_ticket.yaml: -------------------------------------------------------------------------------- 1 | id: close_ticket 2 | type: MongoDBUpdateOne 3 | connectionId: tickets 4 | payload: 5 | id: 6 | _state: selected_id 7 | properties: 8 | filter: 9 | _id: 10 | _payload: id 11 | update: 12 | $set: 13 | updated_date: 14 | _date: now 15 | status: Closed 16 | $push: 17 | history: 18 | action: Closed ticket 19 | status: Closed 20 | timestamp: 21 | _date: now 22 | -------------------------------------------------------------------------------- /pages/tickets/requests/comment_on_ticket.yaml: -------------------------------------------------------------------------------- 1 | id: comment_on_ticket 2 | type: MongoDBUpdateOne 3 | connectionId: tickets 4 | payload: 5 | id: 6 | _state: selected_id 7 | comment: 8 | _state: selected.comment_input 9 | properties: 10 | filter: 11 | _id: 12 | _payload: id 13 | update: 14 | $set: 15 | updated_date: 16 | _date: now 17 | $push: 18 | history: 19 | action: Commented 20 | status: $status 21 | comment: 22 | _payload: comment 23 | timestamp: 24 | _date: now 25 | -------------------------------------------------------------------------------- /pages/tickets/requests/escalate_ticket.yaml: -------------------------------------------------------------------------------- 1 | id: escalate_ticket 2 | type: MongoDBUpdateOne 3 | connectionId: tickets 4 | payload: 5 | id: 6 | _state: selected_id 7 | comment: 8 | _state: selected.escalate_comment 9 | properties: 10 | filter: 11 | _id: 12 | _payload: id 13 | update: 14 | $set: 15 | updated_date: 16 | _date: now 17 | status: Escalated 18 | $push: 19 | history: 20 | action: Escalated 21 | status: Escalated 22 | comment: 23 | _payload: comment 24 | timestamp: 25 | _date: now 26 | -------------------------------------------------------------------------------- /pages/tickets/requests/fetch_selected.yaml: -------------------------------------------------------------------------------- 1 | id: fetch_selected 2 | type: MongoDBAggregation 3 | connectionId: tickets 4 | payload: 5 | id: 6 | _state: selected_id 7 | properties: 8 | pipeline: 9 | - $match: 10 | _id: 11 | _payload: id 12 | - $project: 13 | status: 1 14 | company: 1 15 | name: 1 16 | phone: 1 17 | type: 1 18 | flagged: 1 19 | ticket_description: 1 20 | ticket_id: 1 21 | created_date: 1 22 | updated_date: 1 23 | satisfaction_score: 1 24 | past_orders: 1 25 | history: 26 | $reverseArray: $history 27 | -------------------------------------------------------------------------------- /pages/tickets/requests/fetch_tickets.yaml: -------------------------------------------------------------------------------- 1 | id: fetch_tickets 2 | type: MongoDBAggregation 3 | connectionId: tickets 4 | payload: 5 | search_input: 6 | _state: search_input 7 | status_selector: 8 | _state: status_selector 9 | type_selector: 10 | _state: type_selector 11 | open_closed_selector: 12 | _state: open_closed_selector 13 | flagged_selector: 14 | _state: flagged_selector 15 | properties: 16 | pipeline: 17 | - $match: 18 | $expr: 19 | $and: 20 | - $or: 21 | - $ne: 22 | - $indexOfCP: 23 | - $toLower: $company 24 | - $toLower: 25 | $ifNull: 26 | - _payload: search_input 27 | - '' 28 | - -1 29 | - $ne: 30 | - $indexOfCP: 31 | - $toLower: $name 32 | - $toLower: 33 | $ifNull: 34 | - _payload: search_input 35 | - '' 36 | - -1 37 | - $eq: 38 | - $status 39 | - $ifNull: 40 | - _payload: status_selector 41 | - $status 42 | - $eq: 43 | - $type 44 | - $ifNull: 45 | - _payload: type_selector 46 | - $type 47 | - $cond: 48 | - $eq: 49 | - _payload: open_closed_selector 50 | - Closed 51 | - $eq: 52 | - $status 53 | - Closed 54 | - $ne: 55 | - $status 56 | - Closed 57 | - $cond: 58 | - $in: 59 | - Only flagged 60 | - _payload: flagged_selector 61 | - $flagged 62 | - true 63 | - $project: 64 | _id: 1 65 | name: 1 66 | company: 1 67 | status: 1 68 | flagged: 1 69 | created_date: 1 70 | ticket_id: 1 71 | - $sort: 72 | flagged: -1 73 | created_date: 1 74 | -------------------------------------------------------------------------------- /pages/tickets/requests/set_flagged.yaml: -------------------------------------------------------------------------------- 1 | id: set_flagged 2 | type: MongoDBUpdateOne 3 | connectionId: tickets 4 | payload: 5 | id: 6 | _state: selected_id 7 | flagged: 8 | _state: selected.flagged 9 | properties: 10 | filter: 11 | _id: 12 | _payload: id 13 | update: 14 | $set: 15 | updated_date: 16 | _date: now 17 | flagged: 18 | _not: 19 | _payload: flagged 20 | $push: 21 | history: 22 | action: 23 | _if: 24 | test: 25 | _eq: 26 | - _payload: flagged 27 | - true 28 | then: Removed flag 29 | else: Flagged 30 | status: $status 31 | timestamp: 32 | _date: now 33 | -------------------------------------------------------------------------------- /pages/tickets/tickets.yaml: -------------------------------------------------------------------------------- 1 | id: tickets 2 | type: PageHeaderMenu 3 | properties: 4 | title: Tickets 5 | content: 6 | style: 7 | background: '#f5f5f5' 8 | layout: 9 | contentGutter: 16 10 | 11 | events: 12 | onMount: 13 | - id: init 14 | type: SetState 15 | params: 16 | # Initialize the page state 17 | flagged_selector: [] 18 | open_closed_selector: Open 19 | selected: {} 20 | tickets_loading: true 21 | onMountAsync: 22 | # Fetch the tickets 23 | - id: fetch_tickets 24 | type: Request 25 | params: 26 | - fetch_tickets 27 | - id: set_fetched_tickets 28 | type: SetState 29 | params: 30 | tickets_loading: false 31 | # Set the tickets to state to populate the tickets list 32 | tickets_list: 33 | _request: fetch_tickets 34 | 35 | requests: 36 | # Requests and actions are written in the requests/actions directories and referenced 37 | # to make the files easier to read. 38 | # They could also be used on multiple pages then. 39 | - _ref: pages/tickets/requests/change_status_on_ticket.yaml 40 | - _ref: pages/tickets/requests/close_ticket.yaml 41 | - _ref: pages/tickets/requests/comment_on_ticket.yaml 42 | - _ref: pages/tickets/requests/escalate_ticket.yaml 43 | - _ref: pages/tickets/requests/fetch_tickets.yaml 44 | - _ref: pages/tickets/requests/fetch_selected.yaml 45 | - _ref: pages/tickets/requests/set_flagged.yaml 46 | 47 | areas: 48 | header: 49 | blocks: 50 | - _ref: shared/components/view_github.yaml 51 | 52 | blocks: 53 | # This box contains the search box and list of tickets on the lhs of the screen 54 | - id: list_box 55 | type: Box 56 | layout: 57 | span: 10 58 | contentGutter: 6 59 | blocks: 60 | - id: open_title 61 | type: Markdown 62 | skeleton: 63 | type: SkeletonParagraph 64 | properties: 65 | lines: 1 66 | properties: 67 | content: '###### Tickets' 68 | 69 | - _ref: pages/tickets/components/search.yaml 70 | - _ref: pages/tickets/components/search_results.yaml 71 | 72 | # This box contains the ticket description on the rhs of the screen. 73 | - _ref: pages/tickets/components/selected.yaml 74 | 75 | # Modals that are opened by the action buttons 76 | - _ref: pages/tickets/components/change_status_modal.yaml 77 | - _ref: pages/tickets/components/comment_modal.yaml 78 | - _ref: pages/tickets/components/escalate_modal.yaml 79 | - _ref: pages/tickets/components/confirm_close_modal.yaml 80 | -------------------------------------------------------------------------------- /shared/components/view_github.yaml: -------------------------------------------------------------------------------- 1 | id: affix 2 | type: Affix 3 | blocks: 4 | - id: source_button 5 | type: Button 6 | properties: 7 | icon: AiOutlineGithub 8 | title: View App Source Code 9 | type: default 10 | shape: round 11 | events: 12 | onClick: 13 | - id: link_repo 14 | type: Link 15 | params: 16 | url: https://github.com/lowdefy/lowdefy-example-case-management 17 | newTab: true 18 | --------------------------------------------------------------------------------