├── .gitignore ├── .yarnrc ├── README.markdown ├── build.gradle ├── client-extensions ├── Tiltfile ├── current-tickets-custom-element │ ├── .eslintrc │ ├── .gitignore │ ├── LCP.json │ ├── README.markdown │ ├── assets │ │ └── live.js │ ├── client-extension.dev.yaml │ ├── client-extension.yaml │ ├── index.html │ ├── package.json │ ├── react-live-edit.gif │ ├── src │ │ ├── RecentActivity.jsx │ │ ├── TicketApp.css │ │ ├── TicketApp.jsx │ │ ├── TicketGrid.jsx │ │ ├── listTypeEntries.js │ │ ├── main.jsx │ │ └── tickets.js │ ├── vite.config.js │ └── yarn.lock ├── list-type-batch │ ├── batch │ │ └── list-type-definition.batch-engine-data.json │ └── client-extension.yaml ├── ticket-batch │ ├── LCP.json │ ├── batch │ │ └── ticket-object-definition.batch-engine-data.json │ └── client-extension.yaml ├── ticket-cleanup-cron │ ├── Dockerfile │ ├── LCP.json │ ├── build.gradle │ ├── client-extension.yaml │ ├── source-formatter.properties │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── liferay │ │ │ └── ticket │ │ │ ├── TicketCleanupApplication.java │ │ │ └── TicketCleanupCommandLineRunner.java │ │ └── resources │ │ ├── application-default.properties │ │ └── application.properties ├── ticket-entry-batch │ ├── LCP.json │ ├── batch │ │ └── ticket-object-entry.batch-engine-data.json │ └── client-extension.yaml ├── ticket-etc-node │ ├── Dockerfile │ ├── LCP.json │ ├── app.js │ ├── client-extension.yaml │ ├── config.js │ ├── package-lock.json │ ├── package.json │ └── util │ │ ├── configTreePath.js │ │ ├── liferay-oauth2-resource-server.js │ │ └── logger.js ├── ticket-spring-boot │ ├── Dockerfile │ ├── LCP.json │ ├── build.gradle │ ├── client-extension.yaml │ └── src │ │ └── main │ │ ├── java │ │ └── com │ │ │ └── liferay │ │ │ └── ticket │ │ │ ├── DocumentationReferral.java │ │ │ ├── TicketRestController.java │ │ │ └── TicketSpringBootApplication.java │ │ └── resources │ │ ├── application-default.properties │ │ └── application.properties └── ticket-theme-css │ ├── README.markdown │ ├── client-extension.dev.yaml │ ├── client-extension.yaml │ ├── gulpfile.mjs │ ├── index.html │ ├── liveedit.gif │ ├── package.json │ └── src │ ├── css │ ├── _clay_variables.scss │ └── _custom.scss │ └── img │ └── logo.svg ├── configs └── local │ ├── portal-ext.properties │ └── tomcat │ └── webapps │ └── ROOT │ └── WEB-INF │ ├── classes │ └── META-INF │ │ └── portal-log4j-ext.xml │ └── web.xml ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img ├── application-screenshot.png ├── apply-theme.gif ├── edit-home-page.gif ├── lcp-console-network.png └── ticket-attributes.png ├── package.json ├── settings.gradle └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.iml 2 | **/.ivy 3 | **/.classpath 4 | **/.project 5 | **/.sass-cache 6 | **/.settings 7 | **/bin 8 | **/build 9 | **/build_gradle 10 | **/dist 11 | **/node_modules 12 | **/node_modules_cache 13 | **/test-coverage 14 | **/tmp 15 | **/.web_bundle_build 16 | .DS_Store 17 | .gradle 18 | .idea 19 | /bundles 20 | /gradle-*.properties 21 | /plugins-sdk/**/classes 22 | /plugins-sdk/**/ivy.xml.MD5 23 | /plugins-sdk/**/liferay-hook.xml.processed 24 | /plugins-sdk/build.*.properties 25 | /plugins-sdk/dependencies/**/*.jar 26 | /plugins-sdk/dist 27 | /plugins-sdk/lib 28 | test-results -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | disable-self-update-check true 2 | yarn-offline-mirror "./node_modules_cache" 3 | yarn-offline-mirror-pruning true -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Client Extension Deep Dive Workshop Script 2 | 3 | ## Before Workshop 4 | 5 | _Note that this setup might take as long as 15 minutes so please run 6 | these steps before the workshop!_ 7 | 8 | _If you'd like help with these commands you can reach out on this Liferay community Slack channel: [#devcon-2023-client-extensions-101-workshop](https://liferay-community.slack.com/archives/C058EQJ0MFG)_ 9 | 10 | 1. Clone repo: 11 | 12 | ```bash 13 | git clone https://github.com/LiferayCloud/client-extensions-deep-dive-devcon-2023.git 14 | ``` 15 | 16 | 1. Change into workspace 17 | 18 | ```bash 19 | cd client-extensions-deep-dive-devcon-2023 20 | ``` 21 | 22 | 1. Initialize the bundle _(this downloads dependencies so it might take a while)_ 23 | 24 | ```bash 25 | ./gradlew initBundle 26 | ``` 27 | 28 | 1. Start DXP 29 | 1. Make sure you have java8 or java11 installed and in the system path (`java -version`) to confirm 30 | 31 | 1. Linux/Mac: 32 | 33 | ```bash 34 | ./bundles/tomcat-9.0.73/bin/catalina.sh run 35 | ``` 36 | 37 | 1. Windows: 38 | 39 | ```bash 40 | .\bundles\tomcat-9.0.73\bin\catalina.bat run 41 | ``` 42 | 43 | 1. DXP should pop up automatically on http://localhost:8080 44 | 45 | 1. Log in with user `test@liferay.com` and password `test` 46 | 47 | _Note - if the page is unresponsive you might have to refresh the page once before logging in (known issue)._ 48 | 49 | 1. Change the password to something you can remember 50 | 51 | 1. Build all projects 52 | 53 | ```bash 54 | ./gradlew build 55 | ``` 56 | 57 | 1. That's it! You can leave DXP running if you are about to complete the workshop exercise, or shut it down 58 | 59 | ## Workshop Exercise 60 | 61 | ### Introduction 62 | 63 | > **What are Client Extensions again?**

Client extensions are a generic mechanism for customizating or extending DXP which run outside of DXP.

The extensions are defined in a `client-extension.yaml` file where we specify their properties.

This config file is deployed to DXP in order to give it the configuration information necessary to communicate with our Client Extension. For secure communcation between DXP and your client extension, OAuth2 can be used easily by specifing oAuth2Application* type of client extension. To learn more about Client Extensions visit the [reference documentation](https://learn.liferay.com/w/dxp/building-applications/client-extensions). 64 | 65 | In this workspace we will use Client Extensions to build the following use case: 66 | 67 | - A ticket management system 68 | 69 | - Requirements: 70 | - Defines a Customized Data Schema 71 | - Applies the Corporate brand and style 72 | - Provides a Customized User Application 73 | - Implements Programmatic Documentation Referral 74 | 75 | In the end it should look like the following image: 76 | ![Screenshot](./img/application-screenshot.png) 77 | 78 | ## Defining a Customized Data Schema 79 | 80 | Our domain model is **Ticket** but DXP doesn't have the concept of a Ticket. Traditionally, we would have used Service Builder to model this but today we will use the [Objects](https://learn.liferay.com/web/guest/w/dxp/building-applications/objects) feature of DXP to define it. 81 | 82 | ### Picklist 83 | 84 | We will start the definition of our domain model by creating some [Picklists](https://learn.liferay.com/web/guest/w/dxp/building-applications/objects/picklists/using-picklists#creating-a-picklist). A picklist is a predetermined list of values a user can select, like a vocabulary. We can use Picklists when modelling Objects where an attribute needs to be constrained to specific values. For instance; status, priority, region and so on. 85 | 86 | The Picklists we need are already defined in the project `client-extensions/list-type-batch`. 87 | 88 | This project's `client-extension.yaml` declares a client extension of `type: batch` (see [Batch Client Extensions](https://learn.liferay.com/w/dxp/building-applications/client-extensions/batch-client-extensions)) which is used to import DXP resources without requiring us to write any code. Resources are exported from DXP's **Import/Export Center** (_in the `JSONT` format required for client extensions_) and placed in the project's `batch` directory. Note that batch engine data files are not generated by hand. However, they are intended to be editiable by humans. 89 | 90 | Execute the following commmand from the root of the workspace to deploy the picklists: 91 | 92 | ```bash 93 | ./gradlew :client-extensions:list-type-batch:deploy 94 | ``` 95 | 96 | > Watch the tomcat logs to see that the client extension deployed. 97 | 98 | ### Object Definition 99 | 100 | Now we can deploy our **Ticket** object defeinition which we have already definded for you and put into this project `client-extensions/ticket-batch`. 101 | 102 | Again, this project's `client-extension.yaml` declares a client extension of `type: batch`. It's `batch` directory contains the batch engine data file where the **Ticket** object definition is defined. 103 | 104 | Execute the following commmand from the root of the workspace to deploy the **Ticket** object: 105 | 106 | ```bash 107 | ./gradlew :client-extensions:ticket-batch:deploy 108 | ``` 109 | 110 | > Watch the tomcat logs to see that the client extension deployed. 111 | 112 | When defining a domain model using Objects a set of headless APIs are automatically generated for you without any additional effort. 113 | 114 | You can view these APIs in DXP's built in headless API browser by following this link: [Tickets Headless API](http://localhost:8080/o/api?endpoint=http://localhost:8080/o/c/tickets/openapi.json) 115 | 116 | > Action item: Please view the endpoints of the headless API now. 117 | 118 | We created the first ticket by hand, but in the scenario where you have pre-existing data, you can import it using batch (several of these operations do need to be performed in order) 119 | 120 | - Lets deploy some pre-existing tickets 121 | 122 | ```bash 123 | ./gradlew :client-extensions:ticket-entry-batch:deploy 124 | ``` 125 | 126 | - Now you can see these ticket entires in the DXP UI 127 | 128 | We've acheived our first business requirement: **Define a Customized Data Schema**. Let's move onto the next. 129 | 130 | ## Apply the Corporate brand and style 131 | 132 | Most organizations, after some level of maturity, have established a brand and style which, ideally, is carried through each new project. There are a number of existing client extensions available to support this use case as opposed to traditional Theme module. These are a subset of the client extensions referred to collectively as [Front-End Client Extensions](https://learn.liferay.com/w/dxp/building-applications/client-extensions/front-end-client-extensions). 133 | 134 | The `client-extensions/ticket-theme-css` project's `client-extension.yaml` declares a client extension of `type: themeCSS` (see [Theme CSS Client Extension](https://learn.liferay.com/w/dxp/building-applications/client-extensions/front-end-client-extensions/theme-css-yaml-configuration-reference)) which is used to replace the two core CSS resources from the portal's OOTB themes without modifying DXP. 135 | 136 | Execute the following commmand from the root of the workspace to deploy the tickets-theme-css project: 137 | 138 | ```bash 139 | ./gradlew :client-extensions:ticket-theme-css:deploy 140 | ``` 141 | 142 | > Watch the tomcat logs to see that the client extension deployed. 143 | 144 | At this point let's return to the [main page of our site](http://localhost:8080). Let's apply the tickets-theme-css to the home page as demonstrated in the following video: 145 | ![Apply Theme to All Pages](./img/apply-theme.gif) 146 | 147 | We've acheived our second business requirement: **Apply the Corporate brand and style**. Let's move onto the next. 148 | 149 | ### Provide a Customized User Application 150 | 151 | Our next business requirement is to build a customized user application. Today in DXP, there are low code mechanisms for doing this which directly support objects but which are not yet enabled as client extensions. So today we are going to solve this using the [Custom Element Client Extension](https://learn.liferay.com/w/dxp/building-applications/client-extensions/front-end-client-extensions/custom-element-yaml-configuration-reference) which enables use to build portal applications based on HTML 5 Web Components. In this case, using React. 152 | 153 | The project is the `client-extensions/current-tickets-custom-element`. This project is a Javascript project with a `package.json` file that has a `.scripts.build` property which allows it to be seamlessly integrated into the workspace build (_the workspace handles this integration for you and even handles the precise Javascript build tool installation and initialization & build tasks like `$pkgman install` and `$pkgman run build`. Here in this workspace `$pkgman` is `yarn` out of preference only._) 154 | 155 | While we inspect this project, let's take a short sidebar and consider the `client-extension.yaml`'s `assemble` block (see [Assembling Client Extensions](https://learn.liferay.com/w/dxp/building-applications/client-extensions/working-with-client-extensions#assembling-client-extensions)). 156 | 157 | #### The `assemble` block 158 | 159 | Note that in each of the previous projects we did already have the assemble block but let's take a minute to review it here. As was eluded to in the previous paragraph the workspace _build_ knows how to seamlessly integrate certain _non-Gradle_ builds. This is true of _most_ Front End client extensions. However it doesn't know what to include in the LUFFA. 160 | 161 | ```yaml 162 | assemble: 163 | - from: build/assets 164 | into: static 165 | ``` 166 | 167 | The assemble block allows you to declare what resources need to be included in the LUFFA. 168 | 169 | Execute the following commmand from the root of the workspace to deploy the current-tickets-custom-element project: 170 | 171 | ```bash 172 | ./gradlew :client-extensions:current-tickets-custom-element:deploy 173 | ``` 174 | 175 | > Watch the tomcat logs to see that the client extension deployed. 176 | 177 | At this point let's return to the [main page of our site](http://localhost:8080). Let's remove the main grid section and add the current-tickets-custom-element in place of it as demonstrated in the following video 178 | ![Edit Home Page to Add Custom Element](./img/edit-home-page.gif) 179 | 180 | Note that this app uses the auto-generated **Ticket** headless APIs. 181 | 182 | We've acheived our third business requirement: **Provide a Customized User Application**. Let's move onto the last. 183 | 184 | ## Implement Programmatic Documentation Referral 185 | 186 | Our last business requirement is to implement a business logic that will improve the speed of resolving tickets so that we can serve customers more efficiently using an programmatic strategy to assess ticket details and adding information directly for the customer and maybe reducing the amount of research support agents need to perform in order to resolve the issue. 187 | 188 | The `client-extensions/ticket-spring-boot` project's `client-extension.yaml` declares a client extension of `type: objectAction` (see [Object Action Client Extension](https://learn.liferay.com/w/dxp/building-applications/client-extensions/microservice-client-extensions/object-action-yaml-configuration-reference)) which enables **Object** event handler which is implemented as a REST endpoint to be registered in Liferay. 189 | 190 | Before we proceed we will make one small change to one of the previously deployed client extensions and redeploy it. Edit the file `client-extensions/ticket-batch/batch/ticket-object-definition.batch-engine-data.json`. 191 | 192 | On line `46` change the value of `"active"` from `false` to `true`. Save the file and then (re)execute the command: 193 | 194 | ```bash 195 | ./gradlew :client-extensions:ticket-batch:deploy 196 | ``` 197 | 198 | One small sidebar about this notion of redeployment. It is intended that all deployment operations from the workspace should be idempotent (_or that redeployments should both be effective and **not** result in error_). This is not only important as a mechanism to speed up iterative development, but as a means to move changes between environments; such as moving future changes from a DEV to UAT or UAT to PROD. 199 | 200 | Back to the business logic. 201 | 202 | > Please take a moment to look at the file `client-extensions/ticket-spring-boot/src/main/java/com/liferay/ticket/TicketRestController.java` 203 | 204 | The key takeaways should be that: 205 | 206 | - the body of the request is the payload which contains all the information relevant to the object entry for which the event was triggered 207 | - the endpoint receives and validates JWT tokens which are signed by DXP and issued specifically for the clientId provisioned for the OAuth2Application also specified in the `client-extension.yaml` using the client extension of `type: oAuthApplicationUserAgent` 208 | 209 | **In a separate terminal**, execute the following commmand from the root of the workspace to deploy the ticket-spring-boot project and at the same time start the microservice: 210 | 211 | ```bash 212 | (cd client-extensions/ticket-spring-boot/ && ../../gradlew deploy bootRun) 213 | ``` 214 | 215 | > Watch the tomcat logs to see that the client extension deployed. 216 | 217 | To witness that the microservice will not allow unauthorized requests run the following curl command in a separate terminal while the microservice is running: 218 | 219 | ```bash 220 | curl -v -X POST http://localhost:58081/ticket/object/action/documentation/referral 221 | ``` 222 | 223 | > Note the response returns an error. 224 | 225 | Finally, return to the [main page of our site](http://localhost:8080) and click the `Generate a New Ticket` button. Review the outcome and verify that: 226 | 227 | 1. a ticket was created 228 | 1. the documentation referrals are added 229 | 230 | We've acheived our third business requirement: **Implement Programmatic Documentation Referral**. 231 | 232 | Try making other changes to the projects and redeploying the changes. In the case of the microservice make sure not only to execute the deploy task but also to restart it after any changes. 233 | 234 | ## Ticket cleanup with cron job (Extra credit) 235 | 236 | Now that we can create tickets, at some point, we need to clean them up. Let's create a cron job that will delete all tickets that are marked 'done' or 'duplicate'. To do this we will use a spring boot application that when executed, it will connect to DXP using the generated headless/REST API for ticket objects using another type of client extension `type: oAuthApplicationHeadlessServer`. This type of OAuth2 application is using the client credentials flow and is associated with a special account defined for this purpose (the current default uses the instance admin). One thing that we need to know is that client credential flow in OAuth2 require both a client_id and client_secret, so there will be some additional steps to perform in order to get this working locally. 237 | 238 | The `client-extensions/ticket-cleanup-cron` project's `client-extension.yaml` declares a client extension of `type: oAuth2ApplicationHeadlessServer` (see [OAuth2ApplicationHeadlessServer Client Extension](https://learn.liferay.com/w/dxp/building-applications/client-extensions/configuration-client-extensions/oauth-headless-server-yaml-configuration-reference)) which defines an OAuth2 Application using client credntials flow. 239 | 240 | Execute the following command 241 | 242 | ```bash 243 | ./gradlew :client-extensions:ticket-cleanup-cron:deploy 244 | ``` 245 | 246 | > Note: See tomcat log for when the client extension is deployed. Now the oAuthApplication has been created in DXP. 247 | 248 | If this were an LXC deployment, the cron schedule is specified in the LCP.json and would be scheduled accordingly. Since we are using a local deployment, we will simulate the cron execution by executing the application ourself. However, since this is a client_credentials type of OAuth2 Application we must provide both the client_id and client_secret. In our sample the code already gets the client_id by looking it up via the external reference code. However, we must copy the secret from the DXP UI. 249 | 250 | 1. Go to the DXP UI and navigate to Control Panel > Security > OAuth 2 Administration 251 | 1. Select the `Ticket Cleanup Oauth Application Headless Server` application and click on the `Edit` button for the Client Secret field. Copy the value. 252 | 1. In the terminal run the following command: 253 | 254 | ```bash 255 | ./gradlew :client-extensions:ticket-cleanup-cron:bootRun --args='--ticket-cleanup-oauth-application-headless-server.oauth2.headless.server.client.secret=' 256 | ``` 257 | 258 | > Note: when you run the application you should see a message about the number of tickets that were deleted. 259 | 260 | ```bash 261 | 2023-06-14 18:18:23.027 INFO 29047 --- [ main] c.l.t.TicketCleanupCommandLineRunner : Amount of tickets: 11 262 | 2023-06-14 18:18:23.028 INFO 29047 --- [ main] c.l.t.TicketCleanupCommandLineRunner : Deleting ticket: 44767 263 | 2023-06-14 18:18:23.134 INFO 29047 --- [ main] c.l.t.TicketCleanupCommandLineRunner : Deleting ticket: 44795 264 | ``` 265 | 266 | ## LXC Deployment (Extra Credit) 267 | 268 | In order to deploy to LXC, we need the following as requirements: 269 | 270 | 1. LXC extension environment with LCP credentials 271 | 1. Access to DXP Virtual Instance connected to the LXC extension enviroment 272 | 273 | Assuming you have everything above, we can now deploy our extensions to LXC. The following steps will deploy the client extensions to LXC: 274 | 275 | 1. From root workspace run this command: `./gradlew clean build` 276 | 1. Execute `lcp login` and enter your credentials 277 | 1. Execute `lcp deploy --extension ` and select the LXC environment for each client extension zip 278 | 279 | 280 | First lets deplay the list-type-batch extension which is the first one we need to deploy since ticket-batch depends on it. 281 | 282 | ```bash 283 | lcp deploy --extension client-extensions/list-type-batch/dist/list-type-batch.zip 284 | ``` 285 | 286 | In the LCP console logs for this extension wait until you see 287 | 288 | ```bash 289 | Jun 16 16:53:26.429 build-58 [listtypebatch-vhp9k] Execute Status: STARTED 290 | Jun 16 16:53:27.228 build-58 [listtypebatch-vhp9k] Execute Status: COMPLETED 291 | ``` 292 | 293 | Next lets deploy the ticket-batch extension 294 | 295 | ```bash 296 | lcp deploy --extension client-extensions/ticket-batch/dist/ticket-batch.zip 297 | ``` 298 | 299 | In the LCP console logs for this extension wait until you see 300 | 301 | ```bash 302 | Jun 16 16:59:24.734 build-59 [ticketbatch-cnhtt] Execute Status: STARTED 303 | Jun 16 16:59:25.532 build-59 [ticketbatch-cnhtt] Execute Status: COMPLETED 304 | ``` 305 | 306 | Now that we have deployed both of the batch type extensions, lets verify in the DXP UI that our object has been imported. 307 | 308 | 1. Go to the DXP UI and navigate to Control Panel > Object > Objects 309 | 1. Verify that the Ticket object is listed 310 | 311 | Next we can deploy both of the frontend client extensions at the same time. 312 | 313 | ```bash 314 | lcp deploy --extension client-extensions/current-tickets-custom-element/dist/current-tickets-custom-element.zip 315 | lcp deploy --extension client-extensions/ticket-theme-css/dist/ticket-theme-css.zip 316 | ``` 317 | 318 | Since these are frontend client extensions, the resources will be loaded by the browser, so we need to make sure the client extension workloads (a Caddy fileserver) are visible on the network (which means the dns entries and global loadblancer will resolve the requests). You can view this using the network tag of the LCP Console: 319 | 320 | https://console.liferay.cloud/projects//network/endpoints 321 | 322 | Wait until you see both the ingress endpoints are green. 323 | 324 | ![Network](./img/lcp-console-network.png) 325 | 326 | Now we can deploy the microservice client extension. 327 | 328 | ```bash 329 | lcp deploy --extension client-extensions/ticket-spring-boot/dist/ticket-spring-boot.zip 330 | ``` 331 | 332 | If it isn't working, see the troubleshooting section down below. If it is working you should see the servie available and in the logs you should see a message like this: 333 | 334 | ```bash 335 | Jun 16 17:46:26.730 build-65 [ticketspringboot-74fcf56d76-tll5v] 2023-06-16 22:46:26.729 INFO 8 --- [ main] rayOAuth2ResourceServerEnableWebSecurity : Using client ID id-99677fc4-b15d-5968-4a1b-88e63897f9 336 | ``` 337 | 338 | This means your microservice is correctly talking with DXP and will be able to verify JWT tokens. 339 | 340 | ### Self-Hosted (local tomcat) Troubleshooting 341 | 342 | #### Batch deployment throws error 343 | 344 | If you deploy the batch client extension to the local tomcat/osgi/client-extensions or dockerDeploy before you start the server, you may see an error when it tries to process the batch client extension. This is a known issue where the batch client extension is processed too soon by the headless batch import process. To fix this, simply reploy the batch client extension using `gradlew deploy` again. 345 | 346 | #### Batch Order is not correct 347 | 348 | If you try to deploy `ticket-batch` or `ticket-entry-batch` client extensions before you deploy the `list-type-batch` this will result in an error because `ticket-batch` depends on `list-type-batch` resources that must be deployed first. This is a known issue that will be addressed in the future. 349 | 350 | #### OAuth2 Scopes are not applied 351 | 352 | If you receive a HTTP 401 error or 403 not allowed, this may be because the OAuth2 scopes were not properly applied. To fix this you must edit the OAuthApplication in the DXP control panel UI and go to the "Scopes" tab and make sure the scopes that you are expected to be set, have indeed be set. In this example application is ths `Ticket User Agent application` and the Scopes that should be set are the `C_Ticket.everything` 353 | 354 | ## LXC Troubleshooting 355 | 356 | Here are some possible problems you may run into when deploying to LXC and how to try to troubleshoot them. 357 | 358 | ### Spring boot microservice not starting (no logs show) 359 | 360 | #### Possible error in DXP 361 | 362 | If you do not see your microservice client extension is starting (lcp deployment never finishes), it is likely because DXP did not process your client-extension configuration correctly or had some error. You can check the DXP logs to see if there is an error processing your client extension configuration. 363 | 364 | #### Possible error in DXP server configuration 365 | 366 | It is possible that the DXP environment in the cloud is not configured correctly, namely the DXP virtual instance may work in the UI but the headless apis are not working, perhaps because of some middleware. Ensure that the `/o/oauth2` headless apis are working by executing the following command: 367 | 368 | ```bash 369 | curl https://dxp-env.lfr.cloud/o/oauth2/jwks 370 | ``` 371 | 372 | This should return the JSON Web Key Set (JWKS) for the DXP environment. If it does not, then the headless apis are not working and you will need to troubleshoot the DXP environment. 373 | 374 | ```bash 375 | {"keys":[{"kty":"RSA","kid":"authServer","alg":"RS256","n":...}]} 376 | ``` 377 | 378 | Here you could use the internal diagnostics tool to try to determine why the microservice is not starting once it is generally available. 379 | 380 | ### Spring Boot microservice starts but is killed (not enough memory) 381 | 382 | If you the LCP console logs for the spring-boot microservice you see that is starts, but it shows that the spring-boot process is being killed like this: 383 | 384 | ```bash 385 | Jun 16 17:22:22.897 build-62 [ticketspringboot-7c9d7f4999-pqcv2] [INFO tini (1)] Spawned child process '/usr/local/bin/liferay_jar_runner_entrypoint.sh' with pid '7' 386 | Jun 16 17:22:22.897 build-62 [ticketspringboot-7c9d7f4999-pqcv2] [INFO tini (1)] Main child exited with signal (with signal 'Terminated') 387 | ``` 388 | 389 | It could be because the pod does not have enough memory. Edit the `client-extensions/ticket-spring-boot/LCP.json` and set the memory to a higher amount and redploy. 390 | 391 | ```bash 392 | ./gradlew :client-extensions:ticket-spring-boot:build 393 | lcp deploy --extension client-extensions/ticket-spring-boot/dist/ticket-spring-boot.zip 394 | ``` 395 | 396 | ### Spring boot microservice starts but is killed (/ready endpoint not available) 397 | 398 | If the spring boot microservice is starting but is immediately killed, you may see a message like this: 399 | 400 | ```bash 401 | Jun 16 17:22:22.897 build-62 [ticketspringboot-7c9d7f4999-pqcv2] [INFO tini (1)] Spawned child process '/usr/local/bin/liferay_jar_runner_entrypoint.sh' with pid '7' 402 | Jun 16 17:22:22.897 build-62 [ticketspringboot-7c9d7f4999-pqcv2] [INFO tini (1)] Main child exited with signal (with signal 'Terminated') 403 | ``` 404 | 405 | This may be because LCP could not detect that the service was ready. Review the LCP.json and notice the `/ready` path. Ensure that this path is able to respond to the platform within the specified timeout. 406 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | createDockerContainer { 2 | hostConfig.network = "host" 3 | } -------------------------------------------------------------------------------- /client-extensions/Tiltfile: -------------------------------------------------------------------------------- 1 | load("ext://uibutton", "cmd_button", "text_input", "location") 2 | 3 | dxp_buildargs = { 4 | "DXP_BASE_IMAGE": "liferay/dxp:7.4.13-u75-d5.0.32-20230504180205" 5 | } 6 | 7 | dxp_data_volume = "dxpDataDeepDiveDevcon2023" 8 | 9 | cmd_button( 10 | "Kill DXP!", 11 | argv=[ 12 | "sh", 13 | "-c", 14 | "docker container rm -f dxp-server", 15 | ], 16 | resource="dxp.lfr.dev", 17 | icon_name="delete", 18 | text="Kill DXP!", 19 | ) 20 | 21 | cmd_button( 22 | "Drop DXP Data Volume!", 23 | argv=[ 24 | "sh", 25 | "-c", 26 | "docker volume rm -f %s" % dxp_data_volume, 27 | ], 28 | resource="dxp.lfr.dev", 29 | icon_name="delete", 30 | text="Drop DXP Data Volume!", 31 | ) 32 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app" 4 | ], 5 | "globals": {"Liferay": true} 6 | } -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/.gitignore: -------------------------------------------------------------------------------- 1 | vite.config.js.timestamp-* 2 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/LCP.json: -------------------------------------------------------------------------------- 1 | { 2 | "cpu": 0.1, 3 | "dependencies": [ 4 | "ticketbatch" 5 | ], 6 | "env": { 7 | "LIFERAY_ROUTES_CLIENT_EXTENSION": "/etc/liferay/lxc/ext-init-metadata", 8 | "LIFERAY_ROUTES_DXP": "/etc/liferay/lxc/dxp-metadata" 9 | }, 10 | "environments": { 11 | "dev": { 12 | "loadBalancer": { 13 | "cdn": false, 14 | "targetPort": 80 15 | } 16 | }, 17 | "infra": { 18 | "deploy": false 19 | } 20 | }, 21 | "id": "currentticketscustomelement", 22 | "kind": "Deployment", 23 | "livenessProbe": { 24 | "httpGet": { 25 | "path": "/", 26 | "port": 80 27 | } 28 | }, 29 | "loadBalancer": { 30 | "cdn": true, 31 | "targetPort": 80 32 | }, 33 | "memory": 50, 34 | "readinessProbe": { 35 | "httpGet": { 36 | "path": "/", 37 | "port": 80 38 | } 39 | }, 40 | "scale": 1 41 | } -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/README.markdown: -------------------------------------------------------------------------------- 1 | # HMR Configuration 2 | 3 | 1. Deploy this client extension and start the dev server using `../../gradlew deployDev packageRunDev` 4 | 5 | 1. Add the extension "Current Tickets Live JS" to the head of the page where you have the custom element deployed. 6 | 7 | Now you should be able to edit source code and the React app will update in Liferay immediately. 8 | 9 | Example 10 | 11 | ![Example](./react-live-edit.gif) -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/assets/live.js: -------------------------------------------------------------------------------- 1 | var script = document.createElement('script'); 2 | script.type = 'module'; 3 | 4 | script.textContent = 5 | "import RefreshRuntime from 'http://localhost:5173/@react-refresh'; " + 6 | 'RefreshRuntime.injectIntoGlobalHook(window); ' + 7 | 'window.$RefreshReg$ = () => {}; ' + 8 | 'window.$RefreshSig$ = () => (type) => type; ' + 9 | 'window.__vite_plugin_react_preamble_installed__ = true; '; 10 | 11 | document.head.appendChild(script); 12 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/client-extension.dev.yaml: -------------------------------------------------------------------------------- 1 | current-tickets-custom-element: 2 | urls: 3 | - http://localhost:5173/@vite/client 4 | - http://localhost:5173/src/main.jsx 5 | assemble: 6 | - from: assets 7 | into: static 8 | current-tickets-live-js: 9 | name: Current Tickets Live JS 10 | type: globalJS 11 | url: live.js -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/client-extension.yaml: -------------------------------------------------------------------------------- 1 | # Liferay workspace can build front end projects but does not know 2 | # which front-end build artifacts are relevant for your application. 3 | # 4 | # This assemble block specifies which files need to be included 5 | # in the client extension build artifact. In this case we are specifying 6 | # the build/assets folder where our front-end build artifacts are created. 7 | # 8 | # See https://learn.liferay.com/w/dxp/building-applications/client-extensions/working-with-client-extensions#assembling-client-extensions 9 | # for more information 10 | assemble: 11 | - from: build/assets 12 | into: static 13 | # Here declare our custom element client extension. We 14 | # specify for example which URLs are used to render our 15 | # application and that we use ES modules 16 | current-tickets-custom-element: 17 | cssURLs: 18 | - "*.css" 19 | friendlyURLMapping: current-tickets-custom-element 20 | htmlElementName: current-tickets-custom-element 21 | instanceable: false 22 | name: Current Tickets Custom Element 23 | portletCategoryName: category.client-extensions 24 | type: customElement 25 | urls: 26 | - "*.js" 27 | useESM: true -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Vite + React 12 | 13 | 14 | 15 |
16 | 17 | 18 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@clayui/alert": "^3.93.0", 4 | "@clayui/icon": "^3.56.0", 5 | "@clayui/pagination": "^3.94.0", 6 | "@clayui/pagination-bar": "^3.94.0", 7 | "@yaireo/relative-time": "^1.0.3", 8 | "axios": "^1.4.0", 9 | "react": "^18.2.0", 10 | "react-data-grid": "^7.0.0-beta.30", 11 | "react-dom": "^18.2.0", 12 | "react-query": "^3.39.3" 13 | }, 14 | "devDependencies": { 15 | "@types/react": "^18.0.28", 16 | "@types/react-dom": "^18.0.11", 17 | "@vitejs/plugin-react": "^4.0.0", 18 | "eslint": "^8.41.0", 19 | "eslint-config-react-app": "^7.0.1", 20 | "eslint-plugin-react": "^7.32.2", 21 | "eslint-plugin-react-hooks": "^4.6.0", 22 | "eslint-plugin-react-refresh": "^0.3.4", 23 | "vite": "^4.3.9", 24 | "vite-plugin-eslint": "^1.8.1" 25 | }, 26 | "name": "vite-project", 27 | "prettier": { 28 | "bracketSpacing": false, 29 | "endOfLine": "lf", 30 | "jsxSingleQuote": false, 31 | "quoteProps": "consistent", 32 | "singleQuote": true, 33 | "tabWidth": 4, 34 | "trailingComma": "es5", 35 | "useTabs": true 36 | }, 37 | "private": true, 38 | "scripts": { 39 | "build": "vite build", 40 | "dev": "vite", 41 | "lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0", 42 | "preview": "vite preview" 43 | }, 44 | "type": "module", 45 | "version": "0.0.0" 46 | } 47 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/react-live-edit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiferayCloud/client-extensions-deep-dive-devcon-2023/6623bb959be830b7c2a1cc5eabe464235835b181/client-extensions/current-tickets-custom-element/react-live-edit.gif -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/src/RecentActivity.jsx: -------------------------------------------------------------------------------- 1 | import RelativeTime from '@yaireo/relative-time'; 2 | const relativeTime = new RelativeTime(); 3 | 4 | export const RecentActivity = ({recentTickets}) => { 5 | return ( 6 |
7 |
8 |

Recent Activity

9 |
    10 | {recentTickets.length > 0 && 11 | recentTickets.map((recentTicket, index) => ( 12 |
  • 13 | Ticket #{recentTicket.id} ( 14 | {recentTicket.subject}) was updated 15 | with status "{recentTicket.ticketStatus}" for 16 | support region {recentTicket.supportRegion}{' '} 17 | {relativeTime.from(recentTicket.dateCreated)}. 18 | {recentTicket.suggestions && 19 | recentTicket.suggestions.length > 0 && ( 20 |
    21 | Update: Here are some 22 | suggestions for resources re: this 23 | ticket:  24 | {recentTicket.suggestions.map( 25 | (suggestion, index) => ( 26 | 27 | 35 | {suggestion.text} 36 | 37 | ,  38 | 39 | ) 40 | )} 41 |
    42 | )} 43 |
  • 44 | ))} 45 | {recentTickets.length === 0 && ( 46 | <> 47 |
  • 48 | Ticket #1234 closed with status "Resolved" by 49 | administrator 50 |
  • 51 |
  • 52 | Ticket #4566 closed with status "Won't fix" by 53 | administrator 54 |
  • 55 | 56 | )} 57 |
58 |
59 |
60 | ); 61 | }; 62 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/src/TicketApp.css: -------------------------------------------------------------------------------- 1 | footer, 2 | header, 3 | nav { 4 | border: 1px solid var(--gray-400); 5 | border-radius: 0.25rem; 6 | padding: 0.5rem; 7 | } 8 | 9 | nav ul { 10 | padding: 0; 11 | } 12 | 13 | nav li { 14 | list-style: none; 15 | padding: 0.25rem 0; 16 | } 17 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/src/TicketApp.jsx: -------------------------------------------------------------------------------- 1 | import {useState} from 'react'; 2 | import {useMutation} from 'react-query'; 3 | 4 | import ClayAlert from '@clayui/alert'; 5 | import {ClayPaginationBarWithBasicItems} from '@clayui/pagination-bar'; 6 | 7 | import {useRecentTickets, useTickets, generateNewTicket} from './tickets'; 8 | import './TicketApp.css'; 9 | import {TicketGrid} from './TicketGrid'; 10 | import {RecentActivity} from './RecentActivity'; 11 | 12 | const initialToastState = { 13 | content: null, 14 | show: false, 15 | type: null, 16 | }; 17 | 18 | const initialFilterState = { 19 | field: '', 20 | value: '', 21 | }; 22 | 23 | const filters = [ 24 | { 25 | field: 'ticketStatus', 26 | value: 'open', 27 | text: 'Open issues', 28 | }, 29 | { 30 | field: 'ticketStatus', 31 | value: 'queued', 32 | text: 'Queued issues', 33 | }, 34 | { 35 | field: 'priority', 36 | value: 'major', 37 | text: 'Major Priority issues', 38 | }, 39 | { 40 | field: 'resolution', 41 | value: 'unresolved', 42 | text: 'Unresolved issues', 43 | }, 44 | ]; 45 | 46 | function App({queryClient}) { 47 | const [filter, setFilter] = useState(initialFilterState); 48 | const [page, setPage] = useState(1); 49 | const [pageSize, setPageSize] = useState(20); 50 | const [search, setSearch] = useState(); 51 | const [toastMessage, setToastMessage] = useState(initialToastState); 52 | 53 | const recentTickets = useRecentTickets(); 54 | const tickets = useTickets(page, pageSize, filter, search); 55 | 56 | const mutation = useMutation({ 57 | mutationFn: generateNewTicket, 58 | onSuccess: () => { 59 | queryClient.invalidateQueries(); 60 | setPage(1); 61 | setToastMessage({ 62 | content: 'A new ticket was added!', 63 | show: true, 64 | type: 'success', 65 | }); 66 | }, 67 | }); 68 | 69 | return ( 70 |
71 |
72 | 86 |
87 |
88 |
89 |

Your Tickets

90 | 100 |
101 |
102 |
103 | { 108 | setSearch(event.target.value); 109 | setPage(1); 110 | }} 111 | > 112 | 113 | 114 |
115 | { 124 | setPageSize(pageSize); 125 | }} 126 | onActiveChange={(page) => { 127 | setPage(page); 128 | }} 129 | spritemap={Liferay.Icons.spritemap} 130 | totalItems={tickets.totalCount} 131 | /> 132 |
133 |
134 | 163 |
164 |
165 | 166 | 167 | {toastMessage.show && ( 168 | 169 | setToastMessage(initialToastState)} 173 | spritemap={Liferay.Icons.spritemap} 174 | > 175 | {toastMessage.content} 176 | 177 | 178 | )} 179 |
180 | ); 181 | } 182 | 183 | export default App; 184 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/src/TicketGrid.jsx: -------------------------------------------------------------------------------- 1 | import DataGrid from 'react-data-grid'; 2 | import ClayIcon from '@clayui/icon'; 3 | import 'react-data-grid/lib/styles.css'; 4 | 5 | export const TicketGrid = ({tickets}) => { 6 | return ( 7 | ( 15 | 16 | {row.suggestions && row.suggestions.length > 0 && ( 17 | 22 | )} 23 | {row.subject} 24 | 25 | ), 26 | }, 27 | { 28 | key: 'resolution', 29 | name: 'Resolution', 30 | resizable: true, 31 | width: '15%', 32 | }, 33 | { 34 | key: 'ticketStatus', 35 | name: 'Status', 36 | resizable: true, 37 | width: '15%', 38 | formatter: ({row}) => ( 39 | 40 | {row.ticketStatus === 'Queued' && ( 41 | 46 | )} 47 | {row.ticketStatus} 48 | 49 | ), 50 | }, 51 | { 52 | key: 'priority', 53 | name: 'Priority', 54 | resizable: true, 55 | width: '10%', 56 | }, 57 | {key: 'type', name: 'Type', resizable: true, width: '10%'}, 58 | { 59 | key: 'supportRegion', 60 | name: 'Region', 61 | resizable: true, 62 | width: '10%', 63 | }, 64 | ]} 65 | rows={tickets.rows} 66 | rowKeyGetter={(row) => row.id} 67 | /> 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/src/listTypeEntries.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | async function fetchListTypeEntries(externalReferenceCode) { 4 | const {data} = await axios.get( 5 | `/o/headless-admin-list-type/v1.0/list-type-definitions/by-external-reference-code/${externalReferenceCode}/list-type-entries?p_auth=${Liferay.authToken}` 6 | ); 7 | 8 | return data?.items.map((item) => ({ 9 | key: item.key, 10 | name: item.name, 11 | })); 12 | } 13 | 14 | export const LIST_TICKET_PRIORITIES = 'LIST_TICKET_PRIORITIES'; 15 | export const LIST_TICKET_RESOLUTIONS = 'LIST_TICKET_RESOLUTIONS'; 16 | export const LIST_TICKET_REGIONS = 'LIST_TICKET_REGIONS'; 17 | export const LIST_TICKET_STATUSES = 'LIST_TICKET_STATUSES'; 18 | export const LIST_TICKET_TYPES = 'LIST_TICKET_TYPES'; 19 | 20 | const listTypeDefinitionERCs = [ 21 | LIST_TICKET_PRIORITIES, 22 | LIST_TICKET_RESOLUTIONS, 23 | LIST_TICKET_REGIONS, 24 | LIST_TICKET_STATUSES, 25 | LIST_TICKET_TYPES, 26 | ]; 27 | 28 | export async function fetchListTypeDefinitions() { 29 | const listTypeDefinitions = {}; 30 | for (const listTypeDefinitionERC of listTypeDefinitionERCs) { 31 | listTypeDefinitions[listTypeDefinitionERC] = await fetchListTypeEntries( 32 | listTypeDefinitionERC 33 | ); 34 | } 35 | 36 | return listTypeDefinitions; 37 | } 38 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {createRoot} from 'react-dom/client'; 3 | import TicketApp from './TicketApp.jsx'; 4 | import {QueryClient, QueryClientProvider} from 'react-query'; 5 | 6 | const queryClient = new QueryClient(); 7 | class WebComponent extends HTMLElement { 8 | connectedCallback() { 9 | const root = createRoot(this); 10 | root.render( 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | } 18 | } 19 | const ELEMENT_ID = 'current-tickets-custom-element'; 20 | 21 | if (!customElements.get(ELEMENT_ID)) { 22 | customElements.define(ELEMENT_ID, WebComponent); 23 | } 24 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/src/tickets.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import {useQuery} from 'react-query'; 3 | 4 | import { 5 | LIST_TICKET_PRIORITIES, 6 | LIST_TICKET_REGIONS, 7 | LIST_TICKET_RESOLUTIONS, 8 | LIST_TICKET_TYPES, 9 | fetchListTypeDefinitions, 10 | } from './listTypeEntries'; 11 | 12 | export async function fetchTickets({queryKey}) { 13 | const [, {filter, page, pageSize, search}] = queryKey; 14 | const filterSnippet = 15 | filter && filter.field && filter.value 16 | ? encodeURI(`&filter=${filter.field} eq '${filter.value}'`) 17 | : ''; 18 | const searchSnippet = search ? encodeURI(`&search=${search}`) : ''; 19 | const {data} = await axios.get( 20 | `/o/c/tickets?p_auth=${Liferay.authToken}&pageSize=${pageSize}&sort=dateModified:desc&page=${page}${filterSnippet}${searchSnippet}` 21 | ); 22 | 23 | return data; 24 | } 25 | 26 | export async function fetchRecentTickets() { 27 | const {data} = await axios.get( 28 | `/o/c/tickets?p_auth=${Liferay.authToken}&pageSize=3&page=1&sort=dateModified:desc` 29 | ); 30 | 31 | return data; 32 | } 33 | 34 | let listTypeDefinitions = {}; 35 | 36 | const ticketSubjects = [ 37 | 'My object definition is not deploying in my batch client extension', 38 | 'A theme CSS client extension is not showing on my search page', 39 | "I would like to change my site's icon through a client extension", 40 | 'When updating a custom element React app, the URL metadata is not specified correctly', 41 | 'Liferay is not triggering my Spring Boot app from an Object Action', 42 | 'Client Extensions are amazing - how can I learn more?', 43 | ]; 44 | 45 | function getRandomElement(array) { 46 | return array[Math.floor(Math.random() * array.length)]; 47 | } 48 | 49 | export async function generateNewTicket() { 50 | if (!(LIST_TICKET_PRIORITIES in listTypeDefinitions)) { 51 | listTypeDefinitions = await fetchListTypeDefinitions(); 52 | } 53 | const priorities = listTypeDefinitions[LIST_TICKET_PRIORITIES]; 54 | const regions = listTypeDefinitions[LIST_TICKET_REGIONS]; 55 | const resolutions = listTypeDefinitions[LIST_TICKET_RESOLUTIONS]; 56 | const types = listTypeDefinitions[LIST_TICKET_TYPES]; 57 | 58 | return axios.post(`/o/c/tickets?p_auth=${Liferay.authToken}`, { 59 | priority: { 60 | key: getRandomElement(priorities).key, 61 | }, 62 | resolution: { 63 | key: getRandomElement(resolutions).key, 64 | }, 65 | status: { 66 | code: 0, 67 | }, 68 | subject: getRandomElement(ticketSubjects), 69 | supportRegion: { 70 | key: getRandomElement(regions).key, 71 | }, 72 | ticketStatus: { 73 | key: 'open', 74 | }, 75 | type: { 76 | key: getRandomElement(types).key, 77 | }, 78 | }); 79 | } 80 | 81 | export function useRecentTickets() { 82 | const recentTickets = useQuery(['recentTickets'], fetchRecentTickets, { 83 | refetchInterval: 5000, 84 | refetchOnMount: false, 85 | }); 86 | 87 | if (recentTickets.isSuccess) { 88 | return recentTickets.data?.items.map((ticket) => { 89 | let suggestions = []; 90 | try { 91 | suggestions = JSON.parse(ticket?.suggestions); 92 | } 93 | catch (error) {} 94 | 95 | return { 96 | dateCreated: new Date(ticket.dateCreated), 97 | dateModified: new Date(ticket.dateModified), 98 | description: ticket.description, 99 | id: ticket.id, 100 | priority: ticket.priority?.name, 101 | resolution: ticket.resolution?.name, 102 | subject: ticket.subject, 103 | suggestions, 104 | supportRegion: ticket.supportRegion?.name, 105 | ticketStatus: ticket.ticketStatus?.name, 106 | type: ticket.type?.name, 107 | }; 108 | }); 109 | } 110 | 111 | return []; 112 | } 113 | 114 | /* Return ticket data from a closure. using React state was leading to too many rerenders 115 | or flickering of ui components */ 116 | export const useTickets = (() => { 117 | let ticketData = {rows: [], totalCount: 0}; 118 | 119 | const useTicketsInner = (page, pageSize, filter, search) => { 120 | const tickets = useQuery( 121 | ['tickets', {page, pageSize, filter, search}], 122 | fetchTickets, 123 | {refetchInterval: 5000, refetchOnMount: false} 124 | ); 125 | 126 | if (tickets.isSuccess) { 127 | ticketData = { 128 | totalCount: tickets?.data?.totalCount, 129 | rows: tickets?.data?.items?.map((ticket) => { 130 | let suggestions = []; 131 | try { 132 | suggestions = JSON.parse(ticket?.suggestions); 133 | } 134 | catch (error) {} 135 | 136 | return { 137 | priority: ticket.priority?.name, 138 | description: ticket.description, 139 | resolution: ticket.resolution?.name, 140 | id: ticket.id, 141 | subject: ticket.subject, 142 | supportRegion: ticket.supportRegion?.name, 143 | ticketStatus: ticket.ticketStatus?.name, 144 | type: ticket.type?.name, 145 | suggestions, 146 | }; 147 | }), 148 | }; 149 | } 150 | 151 | return ticketData; 152 | }; 153 | 154 | return useTicketsInner; 155 | })(); 156 | -------------------------------------------------------------------------------- /client-extensions/current-tickets-custom-element/vite.config.js: -------------------------------------------------------------------------------- 1 | import eslint from 'vite-plugin-eslint'; 2 | import react from '@vitejs/plugin-react'; 3 | import {defineConfig} from 'vite'; 4 | 5 | // https://vitejs.dev/config/ 6 | 7 | export default defineConfig({ 8 | build: { 9 | outDir: 'build', 10 | }, 11 | plugins: [eslint(), react()], 12 | server: { 13 | origin: 'http://localhost:5173', 14 | }, 15 | }); 16 | -------------------------------------------------------------------------------- /client-extensions/list-type-batch/batch/list-type-definition.batch-engine-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "create": { 4 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions", 5 | "method": "POST" 6 | }, 7 | "createBatch": { 8 | "href": "/o/headless-batch-engine/v1.0/import-task/com.liferay.headless.admin.list.type.dto.v1_0.ListTypeDefinition", 9 | "method": "POST" 10 | } 11 | }, 12 | "configuration": { 13 | "className": "com.liferay.headless.admin.list.type.dto.v1_0.ListTypeDefinition", 14 | "companyId": 0, 15 | "parameters": { 16 | "containsHeaders": "true", 17 | "createStrategy": "UPSERT", 18 | "onErrorFail": "false", 19 | "taskItemDelegateName": "DEFAULT", 20 | "updateStrategy": "UPDATE" 21 | }, 22 | "userId": 0, 23 | "version": "v1.0" 24 | }, 25 | "facets": [ 26 | ], 27 | "items": [ 28 | { 29 | "actions": { 30 | "get": { 31 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44302", 32 | "method": "GET" 33 | }, 34 | "permissions": { 35 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44302", 36 | "method": "PATCH" 37 | }, 38 | "update": { 39 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44302", 40 | "method": "PUT" 41 | } 42 | }, 43 | "dateCreated": "2023-05-04T19:27:24Z", 44 | "dateModified": "2023-05-04T19:27:24Z", 45 | "externalReferenceCode": "LIST_TICKET_PRIORITIES", 46 | "id": 44302, 47 | "listTypeEntries": [ 48 | { 49 | "externalReferenceCode": "MINOR_PRIORITY", 50 | "key": "minor", 51 | "name": "Minor", 52 | "name_i18n": { 53 | "en-US": "Minor" 54 | }, 55 | "type": "" 56 | }, 57 | { 58 | "externalReferenceCode": "MODERATE_PRIORITY", 59 | "key": "moderate", 60 | "name": "Moderate", 61 | "name_i18n": { 62 | "en-US": "Moderate" 63 | }, 64 | "type": "" 65 | }, 66 | { 67 | "externalReferenceCode": "MAJOR_PRIORITY", 68 | "key": "major", 69 | "name": "Major", 70 | "name_i18n": { 71 | "en-US": "Major" 72 | }, 73 | "type": "" 74 | }, 75 | { 76 | "externalReferenceCode": "CRITICAL_PRIORITY", 77 | "key": "critical", 78 | "name": "Critical", 79 | "name_i18n": { 80 | "en-US": "Critical" 81 | }, 82 | "type": "" 83 | }, 84 | { 85 | "externalReferenceCode": "UNASSIGNED_PRIORITY", 86 | "key": "unassigned", 87 | "name": "Unassigned", 88 | "name_i18n": { 89 | "en-US": "Unassigned" 90 | }, 91 | "type": "" 92 | } 93 | ], 94 | "name": "LIST_TICKET_PRIORITIES", 95 | "name_i18n": { 96 | "en-US": "LIST_TICKET_PRIORITIES" 97 | } 98 | }, 99 | { 100 | "actions": { 101 | "get": { 102 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44303", 103 | "method": "GET" 104 | }, 105 | "permissions": { 106 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44303", 107 | "method": "PATCH" 108 | }, 109 | "update": { 110 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44303", 111 | "method": "PUT" 112 | } 113 | }, 114 | "dateCreated": "2023-05-04T19:27:24Z", 115 | "dateModified": "2023-05-04T19:27:24Z", 116 | "externalReferenceCode": "LIST_TICKET_RESOLUTIONS", 117 | "id": 44303, 118 | "listTypeEntries": [ 119 | { 120 | "externalReferenceCode": "UNRESOLVED_RESOLUTION", 121 | "key": "unresolved", 122 | "name": "Unresolved", 123 | "name_i18n": { 124 | "en-US": "Unresolved" 125 | }, 126 | "type": "" 127 | }, 128 | { 129 | "externalReferenceCode": "FIXED_RESOLUTION", 130 | "key": "fixed", 131 | "name": "Fixed", 132 | "name_i18n": { 133 | "en-US": "Fixed" 134 | }, 135 | "type": "" 136 | }, 137 | { 138 | "externalReferenceCode": "DUPLICATE_RESOLUTION", 139 | "key": "duplicate", 140 | "name": "Duplicate", 141 | "name_i18n": { 142 | "en-US": "Duplicate" 143 | }, 144 | "type": "" 145 | }, 146 | { 147 | "externalReferenceCode": "EXPIRED_RESOLUTION", 148 | "key": "expired", 149 | "name": "Expired", 150 | "name_i18n": { 151 | "en-US": "Expired" 152 | }, 153 | "type": "" 154 | }, 155 | { 156 | "externalReferenceCode": "DONE_RESOLUTION", 157 | "key": "done", 158 | "name": "Done", 159 | "name_i18n": { 160 | "en-US": "Done" 161 | }, 162 | "type": "" 163 | }, 164 | { 165 | "externalReferenceCode": "APPROVED_RESOLUTION", 166 | "key": "approved", 167 | "name": "Approved", 168 | "name_i18n": { 169 | "en-US": "Approved" 170 | }, 171 | "type": "" 172 | }, 173 | { 174 | "externalReferenceCode": "REJECTED_RESOLUTION", 175 | "key": "rejected", 176 | "name": "Rejected", 177 | "name_i18n": { 178 | "en-US": "Rejected" 179 | }, 180 | "type": "" 181 | } 182 | ], 183 | "name": "LIST_TICKET_RESOLUTIONS", 184 | "name_i18n": { 185 | "en-US": "LIST_TICKET_RESOLUTIONS" 186 | } 187 | }, 188 | { 189 | "actions": { 190 | "get": { 191 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44304", 192 | "method": "GET" 193 | }, 194 | "permissions": { 195 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44304", 196 | "method": "PATCH" 197 | }, 198 | "update": { 199 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44304", 200 | "method": "PUT" 201 | } 202 | }, 203 | "dateCreated": "2023-05-04T19:27:24Z", 204 | "dateModified": "2023-05-04T19:27:24Z", 205 | "externalReferenceCode": "LIST_TICKET_REGIONS", 206 | "id": 44304, 207 | "listTypeEntries": [ 208 | { 209 | "externalReferenceCode": "NORAM_REGION", 210 | "key": "nORAM", 211 | "name": "NORAM", 212 | "name_i18n": { 213 | "en-US": "NORAM" 214 | }, 215 | "type": "" 216 | }, 217 | { 218 | "externalReferenceCode": "LATAM_REGION", 219 | "key": "lATAM", 220 | "name": "LATAM", 221 | "name_i18n": { 222 | "en-US": "LATAM" 223 | }, 224 | "type": "" 225 | }, 226 | { 227 | "externalReferenceCode": "EMEA_REGION", 228 | "key": "eMEA", 229 | "name": "EMEA", 230 | "name_i18n": { 231 | "en-US": "EMEA" 232 | }, 233 | "type": "" 234 | }, 235 | { 236 | "externalReferenceCode": "APAC_REGION", 237 | "key": "aPAC", 238 | "name": "APAC", 239 | "name_i18n": { 240 | "en-US": "APAC" 241 | }, 242 | "type": "" 243 | } 244 | ], 245 | "name": "LIST_TICKET_REGIONS", 246 | "name_i18n": { 247 | "en-US": "LIST_TICKET_REGIONS" 248 | } 249 | }, 250 | { 251 | "actions": { 252 | "get": { 253 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44305", 254 | "method": "GET" 255 | }, 256 | "permissions": { 257 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44305", 258 | "method": "PATCH" 259 | }, 260 | "update": { 261 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44305", 262 | "method": "PUT" 263 | } 264 | }, 265 | "dateCreated": "2023-05-04T19:27:24Z", 266 | "dateModified": "2023-05-04T19:27:24Z", 267 | "externalReferenceCode": "LIST_TICKET_STATUSES", 268 | "id": 44305, 269 | "listTypeEntries": [ 270 | { 271 | "externalReferenceCode": "OPEN_STATUS", 272 | "key": "open", 273 | "name": "Open", 274 | "name_i18n": { 275 | "en-US": "Open" 276 | }, 277 | "type": "" 278 | }, 279 | { 280 | "externalReferenceCode": "VERIFYING_STATUS", 281 | "key": "verifying", 282 | "name": "Verifying", 283 | "name_i18n": { 284 | "en-US": "Verifying" 285 | }, 286 | "type": "" 287 | }, 288 | { 289 | "externalReferenceCode": "QUEUED_STATUS", 290 | "key": "queued", 291 | "name": "Queued", 292 | "name_i18n": { 293 | "en-US": "Queued" 294 | }, 295 | "type": "" 296 | }, 297 | { 298 | "externalReferenceCode": "IN_PROGRESS_STATUS", 299 | "key": "inProgress", 300 | "name": "In Progress", 301 | "name_i18n": { 302 | "en-US": "In Progress" 303 | }, 304 | "type": "" 305 | }, 306 | { 307 | "externalReferenceCode": "ANSWERED_STATUS", 308 | "key": "answered", 309 | "name": "Answered", 310 | "name_i18n": { 311 | "en-US": "Answered" 312 | }, 313 | "type": "" 314 | }, 315 | { 316 | "externalReferenceCode": "ESCALATED_STATUS", 317 | "key": "escalated", 318 | "name": "Escalated", 319 | "name_i18n": { 320 | "en-US": "Escalated" 321 | }, 322 | "type": "" 323 | }, 324 | { 325 | "externalReferenceCode": "IN_REVIEW_STATUS", 326 | "key": "inReview", 327 | "name": "In Review", 328 | "name_i18n": { 329 | "en-US": "In Review" 330 | }, 331 | "type": "" 332 | }, 333 | { 334 | "externalReferenceCode": "WAITING_STATUS", 335 | "key": "waiting", 336 | "name": "Waiting", 337 | "name_i18n": { 338 | "en-US": "Waiting" 339 | }, 340 | "type": "" 341 | }, 342 | { 343 | "externalReferenceCode": "CLOSED_STATUS", 344 | "key": "closed", 345 | "name": "Closed", 346 | "name_i18n": { 347 | "en-US": "Closed" 348 | }, 349 | "type": "" 350 | } 351 | ], 352 | "name": "LIST_TICKET_STATUSES", 353 | "name_i18n": { 354 | "en-US": "LIST_TICKET_STATUSES" 355 | } 356 | }, 357 | { 358 | "actions": { 359 | "get": { 360 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44315", 361 | "method": "GET" 362 | }, 363 | "permissions": { 364 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44315", 365 | "method": "PATCH" 366 | }, 367 | "update": { 368 | "href": "http://localhost:8080/o/headless-admin-list-type/v1.0/list-type-definitions/44315", 369 | "method": "PUT" 370 | } 371 | }, 372 | "dateCreated": "2023-05-04T19:27:24Z", 373 | "dateModified": "2023-05-04T19:27:24Z", 374 | "externalReferenceCode": "LIST_TICKET_TYPES", 375 | "id": 44315, 376 | "listTypeEntries": [ 377 | { 378 | "externalReferenceCode": "APPLICATION_TYPE", 379 | "key": "application", 380 | "name": "Application", 381 | "name_i18n": { 382 | "en-US": "Application" 383 | }, 384 | "type": "" 385 | }, 386 | { 387 | "externalReferenceCode": "CREDIT_LIMIT_TYPE", 388 | "key": "creditLimitIncrease", 389 | "name": "Credit Limit Increase", 390 | "name_i18n": { 391 | "en-US": "Credit Limit Increase" 392 | }, 393 | "type": "" 394 | }, 395 | { 396 | "externalReferenceCode": "ACCOUNT_ISSUE_TYPE", 397 | "key": "accountIssue", 398 | "name": "Account Issue", 399 | "name_i18n": { 400 | "en-US": "Account Issue" 401 | }, 402 | "type": "" 403 | }, 404 | { 405 | "externalReferenceCode": "PRODUCT_ISSUE_TYPE", 406 | "key": "productIssue", 407 | "name": "Product Issue", 408 | "name_i18n": { 409 | "en-US": "Product Issue" 410 | }, 411 | "type": "" 412 | }, 413 | { 414 | "externalReferenceCode": "ORDER_ISSUE_TYPE", 415 | "key": "orderIssue", 416 | "name": "Order Issue", 417 | "name_i18n": { 418 | "en-US": "Order Issue" 419 | }, 420 | "type": "" 421 | }, 422 | { 423 | "externalReferenceCode": "DELIVERY_ISSUE_TYPE", 424 | "key": "deliveryIssue", 425 | "name": "Delivery Issue", 426 | "name_i18n": { 427 | "en-US": "Delivery Issue" 428 | }, 429 | "type": "" 430 | }, 431 | { 432 | "externalReferenceCode": "SITE_ISSUE_TYPE", 433 | "key": "siteIssue", 434 | "name": "Site Issue", 435 | "name_i18n": { 436 | "en-US": "Site Issue" 437 | }, 438 | "type": "" 439 | }, 440 | { 441 | "externalReferenceCode": "OTHER_TYPE", 442 | "key": "other", 443 | "name": "Other", 444 | "name_i18n": { 445 | "en-US": "Other" 446 | }, 447 | "type": "" 448 | } 449 | ], 450 | "name": "LIST_TICKET_TYPES", 451 | "name_i18n": { 452 | "en-US": "LIST_TICKET_TYPES" 453 | } 454 | } 455 | ], 456 | "lastPage": 1, 457 | "page": 1, 458 | "pageSize": 20, 459 | "totalCount": 5 460 | } -------------------------------------------------------------------------------- /client-extensions/list-type-batch/client-extension.yaml: -------------------------------------------------------------------------------- 1 | # This assemble block specifies which files need to be included 2 | # in the client extension build artifact. In this case the batch folder 3 | # contains the JSON definition of our picklists. 4 | assemble: 5 | - from: batch 6 | into: batch 7 | # We are defining a batch import client extension for importing 8 | # our picklists into Liferay. This extension makes use of an 9 | # oAuth profile for importing data. 10 | list-type-batch: 11 | name: List Type Batch 12 | oAuthApplicationHeadlessServer: list-type-batch-importer 13 | type: batch 14 | # Here we declare the oAuth profile we need for importing 15 | # our batch data. When this client extension deploys, this oAuth 16 | # profile will be created. 17 | list-type-batch-importer: 18 | .serviceAddress: localhost:8080 19 | .serviceScheme: http 20 | name: List Type Batch Importer Application 21 | scopes: 22 | - Liferay.Headless.Admin.List.Type.everything 23 | - Liferay.Headless.Batch.Engine.everything 24 | type: oAuthApplicationHeadlessServer -------------------------------------------------------------------------------- /client-extensions/ticket-batch/LCP.json: -------------------------------------------------------------------------------- 1 | { 2 | "cpu": 0.2, 3 | "dependencies": [ 4 | "listtypebatch" 5 | ], 6 | "env": { 7 | "LIFERAY_BATCH_OAUTH_APP_ERC": "ticket-batch-importer" 8 | }, 9 | "environments": { 10 | "infra": { 11 | "deploy": false 12 | } 13 | }, 14 | "id": "ticketbatch", 15 | "kind": "Job", 16 | "memory": 300, 17 | "scale": 1 18 | } -------------------------------------------------------------------------------- /client-extensions/ticket-batch/batch/ticket-object-definition.batch-engine-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "createBatch": { 4 | "href": "/o/headless-batch-engine/v1.0/import-task/com.liferay.object.admin.rest.dto.v1_0.ObjectDefinition", 5 | "method": "POST" 6 | } 7 | }, 8 | "configuration": { 9 | "className": "com.liferay.object.admin.rest.dto.v1_0.ObjectDefinition", 10 | "companyId": 0, 11 | "parameters": { 12 | "containsHeaders": "true", 13 | "createStrategy": "UPSERT", 14 | "onErrorFail": "false", 15 | "taskItemDelegateName": "DEFAULT", 16 | "updateStrategy": "UPDATE" 17 | }, 18 | "userId": 0, 19 | "version": "v1.0" 20 | }, 21 | "items": [ 22 | { 23 | "accountEntryRestricted": false, 24 | "actions": { 25 | "delete": { 26 | }, 27 | "get": { 28 | }, 29 | "permissions": { 30 | }, 31 | "update": { 32 | } 33 | }, 34 | "active": true, 35 | "defaultLanguageId": "en_US", 36 | "enableCategorization": true, 37 | "enableComments": true, 38 | "enableObjectEntryHistory": true, 39 | "externalReferenceCode": "C_TICKET", 40 | "label": { 41 | "en_US": "Ticket" 42 | }, 43 | "name": "Ticket", 44 | "objectActions": [ 45 | { 46 | "active": false, 47 | "conditionExpression": "ticketStatus == 'open'", 48 | "description": "", 49 | "errorMessage": { 50 | }, 51 | "label": { 52 | "en_US": "Ticket Spring Boot Object Action Documentation Referral" 53 | }, 54 | "name": "TicketSpringBootDocumentationReferral", 55 | "objectActionExecutorKey": "function#ticket-spring-boot-object-action-documentation-referral", 56 | "objectActionTriggerKey": "onAfterAdd", 57 | "parameters": { 58 | }, 59 | "status": { 60 | "code": 0, 61 | "label": "never-ran", 62 | "label_i18n": "Never Ran" 63 | } 64 | } 65 | ], 66 | "objectFields": [ 67 | { 68 | "DBType": "Long", 69 | "businessType": "Attachment", 70 | "externalReferenceCode": "d4ada6f2-d1d4-bce9-8725-f38638dac071", 71 | "indexed": true, 72 | "indexedAsKeyword": false, 73 | "indexedLanguageId": "en_US", 74 | "label": { 75 | "en_US": "Attachment" 76 | }, 77 | "name": "attachment", 78 | "objectFieldSettings": [ 79 | { 80 | "name": "acceptedFileExtensions", 81 | "value": "jpeg, jpg, pdf, png" 82 | }, 83 | { 84 | "name": "maximumFileSize", 85 | "value": "100" 86 | }, 87 | { 88 | "name": "fileSource", 89 | "value": "userComputer" 90 | }, 91 | { 92 | "name": "showFilesInDocumentsAndMedia", 93 | "value": "true" 94 | }, 95 | { 96 | "name": "storageDLFolderPath", 97 | "value": "/Ticket" 98 | } 99 | ], 100 | "required": false, 101 | "state": false, 102 | "system": false, 103 | "type": "Long" 104 | }, 105 | { 106 | "DBType": "Date", 107 | "businessType": "Date", 108 | "externalReferenceCode": "0dbb00a5-1bbb-76af-4d8e-d1ed8f955ebe", 109 | "indexed": false, 110 | "indexedAsKeyword": false, 111 | "indexedLanguageId": "", 112 | "label": { 113 | "en_US": "Create Date" 114 | }, 115 | "name": "createDate", 116 | "objectFieldSettings": [ 117 | ], 118 | "required": false, 119 | "state": false, 120 | "system": true, 121 | "type": "Date" 122 | }, 123 | { 124 | "DBType": "String", 125 | "businessType": "Text", 126 | "externalReferenceCode": "590f9b88-b0bf-e6ac-4274-7f5f76e97ecb", 127 | "indexed": false, 128 | "indexedAsKeyword": false, 129 | "indexedLanguageId": "", 130 | "label": { 131 | "en_US": "Author" 132 | }, 133 | "name": "creator", 134 | "objectFieldSettings": [ 135 | ], 136 | "required": false, 137 | "state": false, 138 | "system": true, 139 | "type": "String" 140 | }, 141 | { 142 | "DBType": "Clob", 143 | "businessType": "RichText", 144 | "externalReferenceCode": "1e40d57d-84ca-a929-d540-156357c3ed00", 145 | "indexed": true, 146 | "indexedAsKeyword": false, 147 | "indexedLanguageId": "en_US", 148 | "label": { 149 | "en_US": "Description" 150 | }, 151 | "name": "description", 152 | "objectFieldSettings": [ 153 | ], 154 | "required": false, 155 | "state": false, 156 | "system": false, 157 | "type": "Clob" 158 | }, 159 | { 160 | "DBType": "String", 161 | "businessType": "Text", 162 | "externalReferenceCode": "76c36415-dc8b-d507-ea92-04c0b8b0d3b2", 163 | "indexed": false, 164 | "indexedAsKeyword": false, 165 | "indexedLanguageId": "", 166 | "label": { 167 | "en_US": "External Reference Code" 168 | }, 169 | "name": "externalReferenceCode", 170 | "objectFieldSettings": [ 171 | ], 172 | "required": false, 173 | "state": false, 174 | "system": true, 175 | "type": "String" 176 | }, 177 | { 178 | "DBType": "Long", 179 | "businessType": "LongInteger", 180 | "externalReferenceCode": "e0f7f401-4aa7-b328-11a0-260d944348b9", 181 | "indexed": true, 182 | "indexedAsKeyword": true, 183 | "indexedLanguageId": "", 184 | "label": { 185 | "en_US": "ID" 186 | }, 187 | "name": "id", 188 | "objectFieldSettings": [ 189 | ], 190 | "required": false, 191 | "state": false, 192 | "system": true, 193 | "type": "Long" 194 | }, 195 | { 196 | "DBType": "Date", 197 | "businessType": "Date", 198 | "externalReferenceCode": "d5456e60-dffa-4004-6727-62e7655a3eb0", 199 | "indexed": false, 200 | "indexedAsKeyword": false, 201 | "indexedLanguageId": "", 202 | "label": { 203 | "en_US": "Modified Date" 204 | }, 205 | "name": "modifiedDate", 206 | "objectFieldSettings": [ 207 | ], 208 | "required": false, 209 | "state": false, 210 | "system": true, 211 | "type": "Date" 212 | }, 213 | { 214 | "DBType": "String", 215 | "businessType": "Picklist", 216 | "externalReferenceCode": "e76c634b-0b52-1ef8-f22c-cc39c0f988c8", 217 | "indexed": true, 218 | "indexedAsKeyword": false, 219 | "indexedLanguageId": "en_US", 220 | "label": { 221 | "en_US": "Priority" 222 | }, 223 | "listTypeDefinitionExternalReferenceCode": "LIST_TICKET_PRIORITIES", 224 | "name": "priority", 225 | "objectFieldSettings": [ 226 | ], 227 | "required": true, 228 | "state": false, 229 | "system": false, 230 | "type": "String" 231 | }, 232 | { 233 | "DBType": "String", 234 | "businessType": "Picklist", 235 | "externalReferenceCode": "1a2dcd54-8a82-df4c-7617-3c6e67180eea", 236 | "indexed": true, 237 | "indexedAsKeyword": false, 238 | "indexedLanguageId": "en_US", 239 | "label": { 240 | "en_US": "Resolution" 241 | }, 242 | "listTypeDefinitionExternalReferenceCode": "LIST_TICKET_RESOLUTIONS", 243 | "name": "resolution", 244 | "objectFieldSettings": [ 245 | ], 246 | "required": false, 247 | "state": false, 248 | "system": false, 249 | "type": "String" 250 | }, 251 | { 252 | "DBType": "String", 253 | "businessType": "Text", 254 | "externalReferenceCode": "f302250a-5328-099f-b5e8-2b1fb529cbe5", 255 | "indexed": false, 256 | "indexedAsKeyword": false, 257 | "indexedLanguageId": "", 258 | "label": { 259 | "en_US": "Status" 260 | }, 261 | "name": "status", 262 | "objectFieldSettings": [ 263 | ], 264 | "required": false, 265 | "state": false, 266 | "system": true, 267 | "type": "String" 268 | }, 269 | { 270 | "DBType": "String", 271 | "businessType": "Text", 272 | "externalReferenceCode": "7c87a3b1-7213-2475-2530-e2815305a62a", 273 | "indexed": true, 274 | "indexedAsKeyword": false, 275 | "indexedLanguageId": "en_US", 276 | "label": { 277 | "en_US": "Subject" 278 | }, 279 | "name": "subject", 280 | "objectFieldSettings": [ 281 | ], 282 | "required": true, 283 | "state": false, 284 | "system": false, 285 | "type": "String" 286 | }, 287 | { 288 | "DBType": "Clob", 289 | "businessType": "LongText", 290 | "externalReferenceCode": "suggestions", 291 | "indexed": true, 292 | "indexedAsKeyword": false, 293 | "indexedLanguageId": "en_US", 294 | "label": { 295 | "en_US": "Documentation Suggestions" 296 | }, 297 | "name": "suggestions", 298 | "objectFieldSettings": [ 299 | ], 300 | "required": false, 301 | "state": false, 302 | "system": false, 303 | "type": "Clob" 304 | }, 305 | { 306 | "DBType": "String", 307 | "businessType": "Picklist", 308 | "externalReferenceCode": "61314263-1234-9c0d-2489-07fb5d5648ac", 309 | "indexed": true, 310 | "indexedAsKeyword": false, 311 | "indexedLanguageId": "en_US", 312 | "label": { 313 | "en_US": "Support Region" 314 | }, 315 | "listTypeDefinitionExternalReferenceCode": "LIST_TICKET_REGIONS", 316 | "name": "supportRegion", 317 | "objectFieldSettings": [ 318 | ], 319 | "required": true, 320 | "state": false, 321 | "system": false, 322 | "type": "String" 323 | }, 324 | { 325 | "DBType": "String", 326 | "businessType": "Picklist", 327 | "defaultValue": "open", 328 | "externalReferenceCode": "0a0699b4-c59a-f50c-ed52-046ebca1b804", 329 | "indexed": true, 330 | "indexedAsKeyword": false, 331 | "indexedLanguageId": "en_US", 332 | "label": { 333 | "en_US": "Ticket Status" 334 | }, 335 | "listTypeDefinitionExternalReferenceCode": "LIST_TICKET_STATUSES", 336 | "name": "ticketStatus", 337 | "objectFieldSettings": [ 338 | { 339 | "name": "stateFlow", 340 | "value": { 341 | "objectStates": [ 342 | { 343 | "key": "open", 344 | "objectStateTransitions": [ 345 | { 346 | "key": "verifying" 347 | }, 348 | { 349 | "key": "queued" 350 | }, 351 | { 352 | "key": "inProgress" 353 | }, 354 | { 355 | "key": "answered" 356 | }, 357 | { 358 | "key": "escalated" 359 | }, 360 | { 361 | "key": "inReview" 362 | }, 363 | { 364 | "key": "waiting" 365 | }, 366 | { 367 | "key": "closed" 368 | } 369 | ] 370 | }, 371 | { 372 | "key": "verifying", 373 | "objectStateTransitions": [ 374 | { 375 | "key": "open" 376 | }, 377 | { 378 | "key": "queued" 379 | }, 380 | { 381 | "key": "inProgress" 382 | }, 383 | { 384 | "key": "answered" 385 | }, 386 | { 387 | "key": "escalated" 388 | }, 389 | { 390 | "key": "inReview" 391 | }, 392 | { 393 | "key": "waiting" 394 | }, 395 | { 396 | "key": "closed" 397 | } 398 | ] 399 | }, 400 | { 401 | "key": "queued", 402 | "objectStateTransitions": [ 403 | { 404 | "key": "open" 405 | }, 406 | { 407 | "key": "verifying" 408 | }, 409 | { 410 | "key": "inProgress" 411 | }, 412 | { 413 | "key": "answered" 414 | }, 415 | { 416 | "key": "escalated" 417 | }, 418 | { 419 | "key": "inReview" 420 | }, 421 | { 422 | "key": "waiting" 423 | }, 424 | { 425 | "key": "closed" 426 | } 427 | ] 428 | }, 429 | { 430 | "key": "inProgress", 431 | "objectStateTransitions": [ 432 | { 433 | "key": "open" 434 | }, 435 | { 436 | "key": "verifying" 437 | }, 438 | { 439 | "key": "queued" 440 | }, 441 | { 442 | "key": "answered" 443 | }, 444 | { 445 | "key": "escalated" 446 | }, 447 | { 448 | "key": "inReview" 449 | }, 450 | { 451 | "key": "waiting" 452 | }, 453 | { 454 | "key": "closed" 455 | } 456 | ] 457 | }, 458 | { 459 | "key": "answered", 460 | "objectStateTransitions": [ 461 | { 462 | "key": "open" 463 | }, 464 | { 465 | "key": "verifying" 466 | }, 467 | { 468 | "key": "queued" 469 | }, 470 | { 471 | "key": "inProgress" 472 | }, 473 | { 474 | "key": "escalated" 475 | }, 476 | { 477 | "key": "inReview" 478 | }, 479 | { 480 | "key": "waiting" 481 | }, 482 | { 483 | "key": "closed" 484 | } 485 | ] 486 | }, 487 | { 488 | "key": "escalated", 489 | "objectStateTransitions": [ 490 | { 491 | "key": "open" 492 | }, 493 | { 494 | "key": "verifying" 495 | }, 496 | { 497 | "key": "queued" 498 | }, 499 | { 500 | "key": "inProgress" 501 | }, 502 | { 503 | "key": "answered" 504 | }, 505 | { 506 | "key": "inReview" 507 | }, 508 | { 509 | "key": "waiting" 510 | }, 511 | { 512 | "key": "closed" 513 | } 514 | ] 515 | }, 516 | { 517 | "key": "inReview", 518 | "objectStateTransitions": [ 519 | { 520 | "key": "open" 521 | }, 522 | { 523 | "key": "verifying" 524 | }, 525 | { 526 | "key": "queued" 527 | }, 528 | { 529 | "key": "inProgress" 530 | }, 531 | { 532 | "key": "answered" 533 | }, 534 | { 535 | "key": "escalated" 536 | }, 537 | { 538 | "key": "waiting" 539 | }, 540 | { 541 | "key": "closed" 542 | } 543 | ] 544 | }, 545 | { 546 | "key": "waiting", 547 | "objectStateTransitions": [ 548 | { 549 | "key": "open" 550 | }, 551 | { 552 | "key": "verifying" 553 | }, 554 | { 555 | "key": "queued" 556 | }, 557 | { 558 | "key": "inProgress" 559 | }, 560 | { 561 | "key": "answered" 562 | }, 563 | { 564 | "key": "escalated" 565 | }, 566 | { 567 | "key": "inReview" 568 | }, 569 | { 570 | "key": "closed" 571 | } 572 | ] 573 | }, 574 | { 575 | "key": "closed", 576 | "objectStateTransitions": [ 577 | { 578 | "key": "open" 579 | }, 580 | { 581 | "key": "verifying" 582 | }, 583 | { 584 | "key": "queued" 585 | }, 586 | { 587 | "key": "inProgress" 588 | }, 589 | { 590 | "key": "answered" 591 | }, 592 | { 593 | "key": "escalated" 594 | }, 595 | { 596 | "key": "inReview" 597 | }, 598 | { 599 | "key": "waiting" 600 | } 601 | ] 602 | } 603 | ] 604 | } 605 | } 606 | ], 607 | "required": true, 608 | "state": true, 609 | "system": false, 610 | "type": "String" 611 | }, 612 | { 613 | "DBType": "String", 614 | "businessType": "Picklist", 615 | "externalReferenceCode": "e2246594-ec9c-8ae6-d803-5df513cb6c89", 616 | "indexed": true, 617 | "indexedAsKeyword": false, 618 | "indexedLanguageId": "en_US", 619 | "label": { 620 | "en_US": "Type" 621 | }, 622 | "listTypeDefinitionExternalReferenceCode": "LIST_TICKET_TYPES", 623 | "name": "type", 624 | "objectFieldSettings": [ 625 | ], 626 | "required": true, 627 | "state": false, 628 | "system": false, 629 | "type": "String" 630 | } 631 | ], 632 | "objectLayouts": [ 633 | ], 634 | "objectRelationships": [ 635 | ], 636 | "objectValidationRules": [ 637 | ], 638 | "objectViews": [ 639 | ], 640 | "panelCategoryKey": "control_panel.object", 641 | "parameterRequired": false, 642 | "pluralLabel": { 643 | "en_US": "Tickets" 644 | }, 645 | "portlet": true, 646 | "restContextPath": "/o/c/tickets", 647 | "scope": "company", 648 | "status": { 649 | "code": 0, 650 | "label": "approved", 651 | "label_i18n": "Approved" 652 | }, 653 | "system": false, 654 | "titleObjectFieldName": "id" 655 | } 656 | ] 657 | } -------------------------------------------------------------------------------- /client-extensions/ticket-batch/client-extension.yaml: -------------------------------------------------------------------------------- 1 | # This assemble block specifies which files need to be included 2 | # in the client extension build artifact. In this case the batch folder 3 | # contains the JSON definition of our picklists. 4 | assemble: 5 | - from: batch 6 | into: batch 7 | # We are defining a batch import client extension for importing 8 | # our ticket model into Liferay. This extension makes use of an 9 | # oAuth profile for importing data. 10 | ticket-batch: 11 | name: Ticket Batch 12 | oAuthApplicationHeadlessServer: ticket-batch-importer 13 | type: batch 14 | # Here we declare the oAuth profile we need for importing 15 | # our batch data. When this client extension deploys, this oAuth 16 | # profile will be created. 17 | ticket-batch-importer: 18 | .serviceAddress: localhost:8080 19 | .serviceScheme: http 20 | name: Ticket Batch Importer Application 21 | scopes: 22 | - Liferay.Headless.Batch.Engine.everything 23 | - Liferay.Object.Admin.REST.everything 24 | type: oAuthApplicationHeadlessServer -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM liferay/jar-runner:latest 2 | 3 | COPY *.jar /opt/liferay/jar-runner.jar 4 | 5 | #ENV LIFERAY_JAR_RUNNER_JAVA_OPTS="-Xmx512m" -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/LCP.json: -------------------------------------------------------------------------------- 1 | { 2 | "concurrencyPolicy": "Forbid", 3 | "cpu": 1, 4 | "env": { 5 | "LIFERAY_ROUTES_CLIENT_EXTENSION": "/etc/liferay/lxc/ext-init-metadata", 6 | "LIFERAY_ROUTES_DXP": "/etc/liferay/lxc/dxp-metadata" 7 | }, 8 | "environments": { 9 | "infra": { 10 | "deploy": false 11 | } 12 | }, 13 | "id": "__PROJECT_ID__", 14 | "kind": "CronJob", 15 | "memory": 1024, 16 | "scale": 1, 17 | "schedule": "*/15 * * * *" 18 | } -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath group: "com.liferay", name: "com.liferay.gradle.plugins.defaults", version: "latest.release" 4 | classpath group: "org.springframework.boot", name: "spring-boot-gradle-plugin", version: "2.7.11" 5 | } 6 | 7 | repositories { 8 | maven { 9 | url new File(rootProject.projectDir, "../../.m2-tmp") 10 | } 11 | 12 | maven { 13 | url "https://repository-cdn.liferay.com/nexus/content/groups/public" 14 | } 15 | } 16 | } 17 | 18 | apply plugin: "com.liferay.source.formatter" 19 | apply plugin: "java-library" 20 | apply plugin: "org.springframework.boot" 21 | 22 | bootRun { 23 | String liferayVirtualInstanceId = project.hasProperty("liferay.virtual.instance.id") ? project.getProperty("liferay.virtual.instance.id") : "default" 24 | 25 | environment "LIFERAY_ROUTES_CLIENT_EXTENSION", "${gradle.liferayWorkspace.homeDir}/routes/${liferayVirtualInstanceId}/${project.name}" 26 | environment "LIFERAY_ROUTES_DXP", "${gradle.liferayWorkspace.homeDir}/routes/${liferayVirtualInstanceId}/dxp" 27 | } 28 | 29 | dependencies { 30 | implementation group: "com.liferay", name: "com.liferay.client.extension.util.spring.boot", version: "latest.release" 31 | implementation group: "com.liferay", name: "com.liferay.headless.admin.user.client", version: "latest.release" 32 | implementation group: "com.liferay", name: "com.liferay.headless.delivery.client", version: "latest.release" 33 | implementation group: "commons-logging", name: "commons-logging", version: "1.2" 34 | implementation group: "org.json", name: "json", version: "20230618" 35 | implementation group: "org.springframework.boot", name: "spring-boot", version: "2.7.11" 36 | implementation group: "org.springframework.boot", name: "spring-boot-starter-oauth2-client", version: "2.7.11" 37 | implementation group: "org.springframework.boot", name: "spring-boot-starter-web", version: "2.7.11" 38 | } 39 | 40 | repositories { 41 | maven { 42 | url new File(rootProject.projectDir, "../../.m2-tmp") 43 | } 44 | 45 | maven { 46 | url "https://repository-cdn.liferay.com/nexus/content/groups/public" 47 | } 48 | } -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/client-extension.yaml: -------------------------------------------------------------------------------- 1 | assemble: 2 | - fromTask: bootJar 3 | ticket-cleanup-oauth-application-headless-server: 4 | .serviceAddress: localhost:8080 5 | .serviceScheme: http 6 | name: Ticket Cleanup Oauth Application Headless Server 7 | scopes: 8 | - C_Ticket.everything 9 | type: oAuthApplicationHeadlessServer -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/source-formatter.properties: -------------------------------------------------------------------------------- 1 | checkstyle.chaining.check.allowedMethodNames=\ 2 | stream,\ 3 | withClientRegistrationId -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/src/main/java/com/liferay/ticket/TicketCleanupApplication.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | package com.liferay.ticket; 16 | 17 | import com.liferay.client.extension.util.spring.boot.ClientExtensionUtilSpringBootComponentScan; 18 | 19 | import org.springframework.boot.WebApplicationType; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.boot.builder.SpringApplicationBuilder; 22 | import org.springframework.context.annotation.Import; 23 | 24 | /** 25 | * @author Gregory Amerson 26 | */ 27 | @Import(ClientExtensionUtilSpringBootComponentScan.class) 28 | @SpringBootApplication 29 | public class TicketCleanupApplication { 30 | 31 | public static void main(String[] args) { 32 | new SpringApplicationBuilder( 33 | TicketCleanupApplication.class 34 | ).web( 35 | WebApplicationType.NONE 36 | ).run( 37 | args 38 | ); 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/src/main/java/com/liferay/ticket/TicketCleanupCommandLineRunner.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | package com.liferay.ticket; 16 | 17 | import java.util.Arrays; 18 | import java.util.Objects; 19 | 20 | import org.apache.commons.logging.Log; 21 | import org.apache.commons.logging.LogFactory; 22 | 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.beans.factory.annotation.Value; 25 | import org.springframework.boot.CommandLineRunner; 26 | import org.springframework.http.HttpHeaders; 27 | import org.springframework.http.HttpStatus; 28 | import org.springframework.http.MediaType; 29 | import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; 30 | import org.springframework.security.oauth2.client.OAuth2AuthorizeRequest; 31 | import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; 32 | import org.springframework.security.oauth2.core.OAuth2AccessToken; 33 | import org.springframework.stereotype.Component; 34 | import org.springframework.web.reactive.function.client.WebClient; 35 | 36 | import reactor.core.publisher.Mono; 37 | 38 | /** 39 | * @author Gregory Amerson 40 | */ 41 | @Component 42 | public class TicketCleanupCommandLineRunner implements CommandLineRunner { 43 | 44 | @Override 45 | public void run(String... args) throws Exception { 46 | OAuth2AuthorizedClient oAuth2AuthorizedClient = 47 | _authorizedClientServiceOAuth2AuthorizedClientManager.authorize( 48 | OAuth2AuthorizeRequest.withClientRegistrationId( 49 | "ticket-cleanup-oauth-application-headless-server" 50 | ).principal( 51 | "TicketCleanupCommandLineRunner" 52 | ).build()); 53 | 54 | if (oAuth2AuthorizedClient == null) { 55 | _log.error("Unable to get OAuth 2 authorized client"); 56 | 57 | return; 58 | } 59 | 60 | OAuth2AccessToken oAuth2AccessToken = 61 | oAuth2AuthorizedClient.getAccessToken(); 62 | 63 | if (_log.isInfoEnabled()) { 64 | _log.info("Issued: " + oAuth2AccessToken.getIssuedAt()); 65 | _log.info("Expires At: " + oAuth2AccessToken.getExpiresAt()); 66 | _log.info("Scopes: " + oAuth2AccessToken.getScopes()); 67 | _log.info("Token: " + oAuth2AccessToken.getTokenValue()); 68 | } 69 | 70 | WebClient.Builder builder = WebClient.builder(); 71 | 72 | WebClient webClient = builder.baseUrl( 73 | _lxcDXPServerProtocol + "://" + _lxcDXPMainDomain 74 | ).defaultHeader( 75 | HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE 76 | ).defaultHeader( 77 | HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE 78 | ).defaultHeader( 79 | HttpHeaders.AUTHORIZATION, 80 | "Bearer " + oAuth2AccessToken.getTokenValue() 81 | ).build(); 82 | 83 | TicketsResponse ticketsResponse = webClient.get( 84 | ).uri( 85 | "/o/c/tickets/" 86 | ).retrieve( 87 | ).onStatus( 88 | HttpStatus::isError, 89 | response -> { 90 | if (_log.isErrorEnabled()) { 91 | _log.error( 92 | "Unable to get tickets: " + response.statusCode()); 93 | } 94 | 95 | return Mono.error(new Exception()); 96 | } 97 | ).bodyToMono( 98 | TicketsResponse.class 99 | ).block(); 100 | 101 | if (_log.isInfoEnabled()) { 102 | _log.info("Amount of tickets: " + ticketsResponse.items.length); 103 | } 104 | 105 | Arrays.stream( 106 | ticketsResponse.items 107 | ).filter( 108 | ticket -> 109 | (ticket.resolution != null) && 110 | (Objects.equals(ticket.resolution.key, "duplicate") || 111 | Objects.equals(ticket.resolution.key, "done")) 112 | ).map( 113 | ticket -> ticket.id 114 | ).forEach( 115 | ticketId -> { 116 | try { 117 | if (_log.isInfoEnabled()) { 118 | _log.info("Deleting ticket: " + ticketId); 119 | } 120 | 121 | webClient.delete( 122 | ).uri( 123 | "/o/c/tickets/{ticketId}", ticketId 124 | ).retrieve( 125 | ).onStatus( 126 | HttpStatus::isError, 127 | response -> { 128 | if (_log.isErrorEnabled()) { 129 | _log.error( 130 | "Unable to delete ticket: " + 131 | response.statusCode()); 132 | } 133 | 134 | return Mono.error(new Exception()); 135 | } 136 | ).toEntity( 137 | Void.class 138 | ).block(); 139 | } 140 | catch (Exception exception) { 141 | _log.error(exception.getMessage(), exception); 142 | } 143 | } 144 | ); 145 | } 146 | 147 | private static final Log _log = LogFactory.getLog( 148 | TicketCleanupCommandLineRunner.class); 149 | 150 | @Autowired 151 | private AuthorizedClientServiceOAuth2AuthorizedClientManager 152 | _authorizedClientServiceOAuth2AuthorizedClientManager; 153 | 154 | @Value("${com.liferay.lxc.dxp.mainDomain}") 155 | private String _lxcDXPMainDomain; 156 | 157 | @Value("${com.liferay.lxc.dxp.server.protocol}") 158 | private String _lxcDXPServerProtocol; 159 | 160 | private static class Resolution { 161 | 162 | public String key; 163 | 164 | } 165 | 166 | private static class Ticket { 167 | 168 | public String id; 169 | public Resolution resolution; 170 | 171 | } 172 | 173 | private static class TicketsResponse { 174 | 175 | public Ticket[] items; 176 | 177 | } 178 | 179 | } -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/src/main/resources/application-default.properties: -------------------------------------------------------------------------------- 1 | # 2 | # OAuth 3 | # 4 | 5 | ticket-cleanup-oauth-application-headless-server.oauth2.headless.server.client.secret= 6 | liferay.oauth.application.external.reference.codes=ticket-cleanup-oauth-application-headless-server -------------------------------------------------------------------------------- /client-extensions/ticket-cleanup-cron/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.config.import=\ 2 | classpath:/application-default.properties,\ 3 | optional:configtree:${LIFERAY_ROUTES_CLIENT_EXTENSION}/,\ 4 | optional:configtree:${LIFERAY_ROUTES_DXP}/ -------------------------------------------------------------------------------- /client-extensions/ticket-entry-batch/LCP.json: -------------------------------------------------------------------------------- 1 | { 2 | "cpu": 0.2, 3 | "dependencies": [ 4 | "ticketbatch" 5 | ], 6 | "env": { 7 | "LIFERAY_BATCH_OAUTH_APP_ERC": "ticket-entry-batch-importer", 8 | "LIFERAY_ROUTES_CLIENT_EXTENSION": "/etc/liferay/lxc/ext-init-metadata", 9 | "LIFERAY_ROUTES_DXP": "/etc/liferay/lxc/dxp-metadata" 10 | }, 11 | "environments": { 12 | "infra": { 13 | "deploy": false 14 | } 15 | }, 16 | "id": "ticketentrybatch", 17 | "kind": "Job", 18 | "memory": 300, 19 | "scale": 1 20 | } -------------------------------------------------------------------------------- /client-extensions/ticket-entry-batch/batch/ticket-object-entry.batch-engine-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "create": { 4 | "href": "http://localhost:8080/o/c/tickets/", 5 | "method": "POST" 6 | }, 7 | "createBatch": { 8 | "href": "http://localhost:8080/o/c/tickets/batch", 9 | "method": "POST" 10 | }, 11 | "deleteBatch": { 12 | "href": "http://localhost:8080/o/c/tickets/batch", 13 | "method": "DELETE" 14 | }, 15 | "updateBatch": { 16 | "href": "http://localhost:8080/o/c/tickets/batch", 17 | "method": "PUT" 18 | } 19 | }, 20 | "configuration": { 21 | "className": "com.liferay.object.rest.dto.v1_0.ObjectEntry", 22 | "companyId": 0, 23 | "parameters": { 24 | "containsHeaders": "true", 25 | "createStrategy": "UPSERT", 26 | "onErrorFail": "false", 27 | "taskItemDelegateName": "C_Ticket", 28 | "updateStrategy": "UPDATE" 29 | }, 30 | "taskItemDelegateName": "C_Ticket", 31 | "userId": 0, 32 | "version": "v1.0" 33 | }, 34 | "facets": [ 35 | ], 36 | "items": [ 37 | { 38 | "actions": { 39 | "delete": { 40 | "href": "http://localhost:8080/o/c/tickets/44547", 41 | "method": "DELETE" 42 | }, 43 | "get": { 44 | "href": "http://localhost:8080/o/c/tickets/44547", 45 | "method": "GET" 46 | }, 47 | "permissions": { 48 | "href": "http://localhost:8080/o/c/tickets/44547/permissions", 49 | "method": "GET" 50 | }, 51 | "replace": { 52 | "href": "http://localhost:8080/o/c/tickets/44547", 53 | "method": "PUT" 54 | }, 55 | "update": { 56 | "href": "http://localhost:8080/o/c/tickets/44547", 57 | "method": "PATCH" 58 | } 59 | }, 60 | "creator": { 61 | "additionalName": "", 62 | "contentType": "UserAccount", 63 | "familyName": "Test", 64 | "givenName": "Test", 65 | "id": 20123, 66 | "name": "Test Test" 67 | }, 68 | "dateCreated": "2023-05-04T21:49:04Z", 69 | "dateModified": "2023-05-04T21:49:04Z", 70 | "description": "When I access my site, I receive a 404 error.", 71 | "descriptionRawText": "When I access my site, I receive a 404 error.", 72 | "externalReferenceCode": "91dc74bf-3c37-6ba3-a0ad-0eec02d0bd6b", 73 | "id": 44547, 74 | "keywords": [ 75 | ], 76 | "priority": { 77 | "key": "minor", 78 | "name": "Minor" 79 | }, 80 | "status": { 81 | "code": 0, 82 | "label": "approved", 83 | "label_i18n": "Approved" 84 | }, 85 | "subject": "Web Site is not responding", 86 | "supportRegion": { 87 | "key": "lATAM", 88 | "name": "LATAM" 89 | }, 90 | "taxonomyCategoryBriefs": [ 91 | ], 92 | "ticketStatus": { 93 | "key": "open", 94 | "name": "Open" 95 | }, 96 | "type": { 97 | "key": "siteIssue", 98 | "name": "Site Issue" 99 | } 100 | } 101 | ], 102 | "lastPage": 1, 103 | "page": 1, 104 | "pageSize": 20, 105 | "totalCount": 1 106 | } -------------------------------------------------------------------------------- /client-extensions/ticket-entry-batch/client-extension.yaml: -------------------------------------------------------------------------------- 1 | assemble: 2 | - from: batch 3 | into: batch 4 | ticket-entry-batch: 5 | name: Ticket Entry Batch 6 | oAuthApplicationHeadlessServer: ticket-entry-batch-importer 7 | type: batch 8 | ticket-entry-batch-importer: 9 | .serviceAddress: localhost:8080 10 | .serviceScheme: http 11 | name: Ticket Entry Batch Importer Application 12 | scopes: 13 | - C_Ticket 14 | - Liferay.Headless.Batch.Engine.everything 15 | type: oAuthApplicationHeadlessServer -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM liferay/node-runner:latest 2 | 3 | COPY . /opt/liferay 4 | 5 | RUN npm install -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/LCP.json: -------------------------------------------------------------------------------- 1 | { 2 | "cpu": 1, 3 | "env": { 4 | "LIFERAY_ROUTES_CLIENT_EXTENSION": "/etc/liferay/lxc/ext-init-metadata", 5 | "LIFERAY_ROUTES_DXP": "/etc/liferay/lxc/dxp-metadata" 6 | }, 7 | "environments": { 8 | "infra": { 9 | "deploy": false 10 | } 11 | }, 12 | "id": "ticketetcnode", 13 | "kind": "Deployment", 14 | "livenessProbe": { 15 | "httpGet": { 16 | "path": "/ready", 17 | "port": 3001 18 | } 19 | }, 20 | "loadBalancer": { 21 | "targetPort": 3001 22 | }, 23 | "memory": 512, 24 | "readinessProbe": { 25 | "httpGet": { 26 | "path": "/ready", 27 | "port": 3001 28 | } 29 | }, 30 | "scale": 1 31 | } -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | 16 | import bodyParser from 'body-parser'; 17 | import express from 'express'; 18 | import fetch from 'node-fetch'; 19 | 20 | import config from './util/configTreePath.js'; 21 | import {corsWithReady, liferayJWT} from './util/liferay-oauth2-resource-server.js'; 22 | import {logger} from './util/logger.js'; 23 | 24 | const serverPort = config['server.port']; 25 | const app = express(); 26 | 27 | logger.log(`config: ${JSON.stringify(config, null, '\t')}`); 28 | 29 | app.use(express.json()); 30 | app.use(corsWithReady); 31 | app.use(liferayJWT); 32 | 33 | app.get(config.readyPath, (req, res) => { 34 | res.send('READY'); 35 | }); 36 | 37 | app.post('/ticket/object/action/documentation/referral', async (req, res) => { 38 | logger.log('User %s is authorized', req.jwt.username); 39 | logger.log('User scopes: ' + req.jwt.scope); 40 | 41 | const json = req.body; 42 | logger.log(`/ticket/object/action/documentation/referral: json: ${JSON.stringify(json, null, '\t')}`); 43 | res.status(200).send(json); 44 | }); 45 | 46 | app.listen(serverPort, () => { 47 | logger.log('App listening on %s', serverPort); 48 | }); 49 | 50 | export default app; 51 | -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/client-extension.yaml: -------------------------------------------------------------------------------- 1 | assemble: 2 | - include: 3 | - package.json 4 | - "**/*.js" 5 | ticket-node-oauth-application-user-agent: 6 | .serviceAddress: localhost:3001 7 | .serviceScheme: http 8 | name: Ticket Node OAuth Application User Agent 9 | scopes: 10 | - C_Ticket 11 | type: oAuthApplicationUserAgent 12 | ticket-node-object-action-documentation-referral: 13 | name: Ticket Node Object Action Documentation Referral 14 | oAuth2ApplicationExternalReferenceCode: ticket-node-oauth-application-user-agent 15 | resourcePath: /ticket/object/action/documentation/referral 16 | type: objectAction -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | export default { 15 | 'com.liferay.lxc.dxp.domains': 'localhost:8080', 16 | 'com.liferay.lxc.dxp.mainDomain': 'localhost:8080', 17 | 'com.liferay.lxc.dxp.server.protocol': 'http', 18 | 'configTreePath': '/etc/liferay/lxc', 19 | 'liferay.oauth.application.external.reference.codes': 'ticket-node-oauth-application-user-agent', 20 | 'readyPath': '/ready', 21 | 'server.port': 3001, 22 | }; 23 | -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liferay-sample-etc-node", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "body-parser": { 8 | "version": "1.20.2", 9 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", 10 | "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", 11 | "requires": { 12 | "bytes": "3.1.2", 13 | "content-type": "~1.0.5", 14 | "debug": "2.6.9", 15 | "depd": "2.0.0", 16 | "destroy": "1.2.0", 17 | "http-errors": "2.0.0", 18 | "iconv-lite": "0.4.24", 19 | "on-finished": "2.4.1", 20 | "qs": "6.11.0", 21 | "raw-body": "2.5.2", 22 | "type-is": "~1.6.18", 23 | "unpipe": "1.0.0" 24 | } 25 | }, 26 | "bytes": { 27 | "version": "3.1.2", 28 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 29 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" 30 | }, 31 | "call-bind": { 32 | "version": "1.0.2", 33 | "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", 34 | "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", 35 | "requires": { 36 | "function-bind": "^1.1.1", 37 | "get-intrinsic": "^1.0.2" 38 | } 39 | }, 40 | "content-type": { 41 | "version": "1.0.5", 42 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 43 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" 44 | }, 45 | "debug": { 46 | "version": "2.6.9", 47 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 48 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 49 | "requires": { 50 | "ms": "2.0.0" 51 | } 52 | }, 53 | "depd": { 54 | "version": "2.0.0", 55 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 56 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" 57 | }, 58 | "destroy": { 59 | "version": "1.2.0", 60 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 61 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" 62 | }, 63 | "ee-first": { 64 | "version": "1.1.1", 65 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 66 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 67 | }, 68 | "function-bind": { 69 | "version": "1.1.1", 70 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 71 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" 72 | }, 73 | "get-intrinsic": { 74 | "version": "1.2.0", 75 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz", 76 | "integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==", 77 | "requires": { 78 | "function-bind": "^1.1.1", 79 | "has": "^1.0.3", 80 | "has-symbols": "^1.0.3" 81 | } 82 | }, 83 | "has": { 84 | "version": "1.0.3", 85 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 86 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 87 | "requires": { 88 | "function-bind": "^1.1.1" 89 | } 90 | }, 91 | "has-symbols": { 92 | "version": "1.0.3", 93 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", 94 | "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" 95 | }, 96 | "http-errors": { 97 | "version": "2.0.0", 98 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 99 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 100 | "requires": { 101 | "depd": "2.0.0", 102 | "inherits": "2.0.4", 103 | "setprototypeof": "1.2.0", 104 | "statuses": "2.0.1", 105 | "toidentifier": "1.0.1" 106 | } 107 | }, 108 | "iconv-lite": { 109 | "version": "0.4.24", 110 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 111 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 112 | "requires": { 113 | "safer-buffer": ">= 2.1.2 < 3" 114 | } 115 | }, 116 | "inherits": { 117 | "version": "2.0.4", 118 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 119 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 120 | }, 121 | "media-typer": { 122 | "version": "0.3.0", 123 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 124 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" 125 | }, 126 | "mime-db": { 127 | "version": "1.52.0", 128 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 129 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" 130 | }, 131 | "mime-types": { 132 | "version": "2.1.35", 133 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 134 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 135 | "requires": { 136 | "mime-db": "1.52.0" 137 | } 138 | }, 139 | "ms": { 140 | "version": "2.0.0", 141 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 142 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 143 | }, 144 | "object-inspect": { 145 | "version": "1.12.3", 146 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", 147 | "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" 148 | }, 149 | "on-finished": { 150 | "version": "2.4.1", 151 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 152 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 153 | "requires": { 154 | "ee-first": "1.1.1" 155 | } 156 | }, 157 | "qs": { 158 | "version": "6.11.0", 159 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", 160 | "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", 161 | "requires": { 162 | "side-channel": "^1.0.4" 163 | } 164 | }, 165 | "raw-body": { 166 | "version": "2.5.2", 167 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", 168 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", 169 | "requires": { 170 | "bytes": "3.1.2", 171 | "http-errors": "2.0.0", 172 | "iconv-lite": "0.4.24", 173 | "unpipe": "1.0.0" 174 | } 175 | }, 176 | "safer-buffer": { 177 | "version": "2.1.2", 178 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 179 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 180 | }, 181 | "setprototypeof": { 182 | "version": "1.2.0", 183 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 184 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 185 | }, 186 | "side-channel": { 187 | "version": "1.0.4", 188 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", 189 | "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", 190 | "requires": { 191 | "call-bind": "^1.0.0", 192 | "get-intrinsic": "^1.0.2", 193 | "object-inspect": "^1.9.0" 194 | } 195 | }, 196 | "statuses": { 197 | "version": "2.0.1", 198 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 199 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" 200 | }, 201 | "toidentifier": { 202 | "version": "1.0.1", 203 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 204 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" 205 | }, 206 | "type-is": { 207 | "version": "1.6.18", 208 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 209 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 210 | "requires": { 211 | "media-typer": "0.3.0", 212 | "mime-types": "~2.1.24" 213 | } 214 | }, 215 | "unpipe": { 216 | "version": "1.0.0", 217 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 218 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" 219 | } 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "body-parser": "1.20.2", 4 | "cors": "2.8.5", 5 | "express": "4.18.2", 6 | "jsonwebtoken": "9.0.0", 7 | "jwk-to-pem": "2.0.5", 8 | "node-fetch": "2.6.9", 9 | "winston": "3.8.2" 10 | }, 11 | "license": "LGPL", 12 | "name": "ticket-etc-node", 13 | "scripts": { 14 | "start": "node app.js" 15 | }, 16 | "type": "module", 17 | "version": "1.0.0" 18 | } 19 | -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/util/configTreePath.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | import fs from 'fs'; 16 | import path from 'path'; 17 | 18 | import config from '../config.js'; 19 | 20 | async function* walk(dir) { 21 | if (fs.existsSync(dir) === false) { 22 | return; 23 | } 24 | 25 | const dirents = await fs.promises.opendir(dir, { 26 | withFileTypes: true, 27 | }); 28 | 29 | for await (const dirent of dirents) { 30 | if (dirent.name.startsWith('..')) { 31 | continue; 32 | } 33 | 34 | const entryPath = path.join(dir, dirent.name); 35 | 36 | if (dirent.isDirectory()) { 37 | yield* walk(entryPath); 38 | } 39 | else { 40 | yield entryPath; 41 | } 42 | } 43 | } 44 | 45 | const configTreeMap = async () => { 46 | for await (const configFile of walk(config.configTreePath)) { 47 | const configFileName = configFile.substring( 48 | configFile.lastIndexOf('/') + 1 49 | ); 50 | 51 | config[configFileName] = fs.readFileSync(configFile, 'utf-8'); 52 | } 53 | 54 | return config; 55 | }; 56 | 57 | export default await configTreeMap(); 58 | -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/util/liferay-oauth2-resource-server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | import cors from 'cors'; 16 | import {verify} from 'jsonwebtoken'; 17 | import jwktopem from 'jwk-to-pem'; 18 | import fetch from 'node-fetch'; 19 | 20 | import config from './configTreePath.js'; 21 | import {logger} from './logger.js'; 22 | 23 | const domains = config['com.liferay.lxc.dxp.domains']; 24 | const externalReferenceCode = 25 | config['liferay.oauth.application.external.reference.codes'].split(',')[0]; 26 | const lxcDXPMainDomain = config['com.liferay.lxc.dxp.mainDomain']; 27 | const lxcDXPServerProtocol = config['com.liferay.lxc.dxp.server.protocol']; 28 | 29 | const uriPath = 30 | config[externalReferenceCode + '.oauth2.jwks.uri'] || '/o/oauth2/jwks'; 31 | 32 | const oauth2JWKSURI = `${lxcDXPServerProtocol}://${lxcDXPMainDomain}${uriPath}`; 33 | 34 | const allowList = domains 35 | .split(',') 36 | .map((domain) => `${lxcDXPServerProtocol}://${domain}`); 37 | 38 | const corsOptions = { 39 | origin(origin, callback) { 40 | callback(null, allowList.includes(origin)); 41 | }, 42 | }; 43 | 44 | export async function corsWithReady(req, res, next) { 45 | if (req.originalUrl === config.readyPath) { 46 | return next(); 47 | } 48 | 49 | return cors(corsOptions)(req, res, next); 50 | } 51 | 52 | export async function liferayJWT(req, res, next) { 53 | if (req.path === config.readyPath) { 54 | return next(); 55 | } 56 | 57 | const authorization = req.headers.authorization; 58 | 59 | if (!authorization) { 60 | res.status(401).send('No authorization header'); 61 | 62 | return; 63 | } 64 | 65 | const [, bearerToken] = req.headers.authorization.split('Bearer '); 66 | 67 | try { 68 | const jwksResponse = await fetch(oauth2JWKSURI); 69 | 70 | if (jwksResponse.status === 200) { 71 | const jwks = await jwksResponse.json(); 72 | 73 | const jwksPublicKey = jwktopem(jwks.keys[0]); 74 | 75 | const decoded = verify(bearerToken, jwksPublicKey, { 76 | algorithms: ['RS256'], 77 | ignoreExpiration: true, // TODO we need to use refresh token 78 | }); 79 | 80 | const applicationResponse = await fetch( 81 | `${lxcDXPServerProtocol}://${lxcDXPMainDomain}/o/oauth2/application?externalReferenceCode=${externalReferenceCode}` 82 | ); 83 | 84 | const {client_id} = await applicationResponse.json(); 85 | 86 | if (decoded.client_id === client_id) { 87 | req.jwt = decoded; 88 | 89 | next(); 90 | } 91 | else { 92 | logger.log( 93 | 'JWT token client_id value does not match expected client_id value.' 94 | ); 95 | 96 | res.status(401).send('Invalid authorization'); 97 | } 98 | } 99 | else { 100 | logger.error( 101 | 'Error fetching JWKS %s %s', 102 | jwksResponse.status, 103 | jwksResponse.statusText 104 | ); 105 | 106 | res.status(401).send('Invalid authorization header'); 107 | } 108 | } 109 | catch (error) { 110 | logger.error('Error validating JWT token\n%s', error); 111 | 112 | res.status(401).send('Invalid authorization header'); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /client-extensions/ticket-etc-node/util/logger.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | export const logger = console; 16 | -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM liferay/jar-runner:latest 2 | 3 | #ENV LIFERAY_JAR_RUNNER_JAVA_OPTS="-Xmx512m" 4 | 5 | COPY *.jar /opt/liferay/jar-runner.jar -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/LCP.json: -------------------------------------------------------------------------------- 1 | { 2 | "cpu": 1, 3 | "env": { 4 | "LIFERAY_ROUTES_CLIENT_EXTENSION": "/etc/liferay/lxc/ext-init-metadata", 5 | "LIFERAY_ROUTES_DXP": "/etc/liferay/lxc/dxp-metadata" 6 | }, 7 | "environments": { 8 | "infra": { 9 | "deploy": false 10 | } 11 | }, 12 | "id": "ticketspringboot", 13 | "kind": "Deployment", 14 | "livenessProbe": { 15 | "httpGet": { 16 | "path": "/ready", 17 | "port": 58081 18 | } 19 | }, 20 | "loadBalancer": { 21 | "targetPort": 58081 22 | }, 23 | "memory": 512, 24 | "readinessProbe": { 25 | "httpGet": { 26 | "path": "/ready", 27 | "port": 58081 28 | } 29 | }, 30 | "scale": 1 31 | } -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath group: "com.liferay", name: "com.liferay.gradle.plugins.defaults", version: "latest.release" 4 | classpath group: "org.springframework.boot", name: "spring-boot-gradle-plugin", version: "2.7.11" 5 | } 6 | 7 | repositories { 8 | mavenLocal() 9 | 10 | maven { 11 | url "https://repository-cdn.liferay.com/nexus/content/groups/public" 12 | } 13 | } 14 | } 15 | 16 | apply plugin: "com.liferay.source.formatter" 17 | apply plugin: "java-library" 18 | apply plugin: "org.springframework.boot" 19 | 20 | bootRun { 21 | String liferayVirtualInstanceId = project.hasProperty("liferay.virtual.instance.id") ? project.getProperty("liferay.virtual.instance.id") : "default" 22 | 23 | environment "LIFERAY_ROUTES_CLIENT_EXTENSION", "${gradle.liferayWorkspace.homeDir}/routes/${liferayVirtualInstanceId}/${project.name}" 24 | environment "LIFERAY_ROUTES_DXP", "${gradle.liferayWorkspace.homeDir}/routes/${liferayVirtualInstanceId}/dxp" 25 | } 26 | 27 | dependencies { 28 | implementation group: "com.liferay", name: "com.liferay.client.extension.util.spring.boot", version: "latest.release" 29 | implementation group: "com.liferay", name: "com.liferay.portal.search.rest.client", version: "latest.release" 30 | implementation group: "commons-logging", name: "commons-logging", version: "1.2" 31 | implementation group: "org.json", name: "json", version: "20230618" 32 | implementation group: "org.springframework.boot", name: "spring-boot", version: "2.7.11" 33 | implementation group: "org.springframework.boot", name: "spring-boot-starter-oauth2-client", version: "2.7.11" 34 | implementation group: "org.springframework.boot", name: "spring-boot-starter-web", version: "2.7.11" 35 | } 36 | 37 | repositories { 38 | mavenLocal() 39 | 40 | maven { 41 | url "https://repository-cdn.liferay.com/nexus/content/groups/public" 42 | } 43 | } -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/client-extension.yaml: -------------------------------------------------------------------------------- 1 | assemble: 2 | - fromTask: bootJar 3 | ticket-spring-boot-oauth-application-user-agent: 4 | .serviceAddress: localhost:58081 5 | .serviceScheme: http 6 | name: Ticket Spring Boot OAuth Application User Agent 7 | scopes: 8 | - C_Ticket.everything 9 | type: oAuthApplicationUserAgent 10 | ticket-spring-boot-object-action-documentation-referral: 11 | name: Ticket Spring Boot Object Action Documentation Referral 12 | oAuth2ApplicationExternalReferenceCode: ticket-spring-boot-oauth-application-user-agent 13 | resourcePath: /ticket/object/action/documentation/referral 14 | type: objectAction -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/src/main/java/com/liferay/ticket/DocumentationReferral.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | package com.liferay.ticket; 16 | 17 | import com.liferay.portal.search.rest.client.dto.v1_0.Suggestion; 18 | import com.liferay.portal.search.rest.client.dto.v1_0.SuggestionsContributorConfiguration; 19 | import com.liferay.portal.search.rest.client.dto.v1_0.SuggestionsContributorResults; 20 | import com.liferay.portal.search.rest.client.pagination.Page; 21 | import com.liferay.portal.search.rest.client.resource.v1_0.SuggestionResource; 22 | 23 | import java.time.Duration; 24 | 25 | import java.util.Objects; 26 | 27 | import org.apache.commons.logging.Log; 28 | import org.apache.commons.logging.LogFactory; 29 | 30 | import org.json.JSONArray; 31 | import org.json.JSONObject; 32 | 33 | import org.springframework.http.HttpHeaders; 34 | import org.springframework.http.HttpStatus; 35 | import org.springframework.http.MediaType; 36 | import org.springframework.web.reactive.function.client.WebClient; 37 | import org.springframework.web.reactive.function.client.WebClientResponseException; 38 | 39 | import reactor.core.publisher.Mono; 40 | 41 | import reactor.util.retry.Retry; 42 | 43 | /** 44 | * @author Raymond Augé 45 | * @author Gregory Amerson 46 | * @author Allen Ziegenfus 47 | */ 48 | public class DocumentationReferral { 49 | 50 | public static final String SUGGESTION_HOST = "learn.liferay.com"; 51 | 52 | public static final int SUGGESTION_PORT = 443; 53 | 54 | public static final String SUGGESTION_SCHEME = "https"; 55 | 56 | public DocumentationReferral() { 57 | _initResourceBuilders(); 58 | } 59 | 60 | public void addDocumentationReferralAndQueue( 61 | String lxcDXPServerProtocol, String lxcDXPMainDomain, String jwtToken, 62 | JSONObject jsonObject) { 63 | 64 | Objects.requireNonNull(jsonObject); 65 | 66 | JSONObject jsonTicketDTO = jsonObject.getJSONObject( 67 | "objectEntryDTOTicket"); 68 | 69 | JSONObject jsonProperties = jsonTicketDTO.getJSONObject("properties"); 70 | 71 | JSONObject jsonTicketStatus = jsonProperties.getJSONObject( 72 | "ticketStatus"); 73 | 74 | String subject = jsonProperties.getString("subject"); 75 | 76 | jsonTicketStatus.remove("name"); 77 | jsonTicketStatus.put("key", "queued"); 78 | jsonProperties.put("suggestions", _getSuggestionsJSON(subject)); 79 | 80 | _log.info("JSON OUTPUT: \n\n" + jsonProperties.toString(4) + "\n"); 81 | 82 | WebClient.Builder builder = WebClient.builder(); 83 | 84 | WebClient webClient = builder.baseUrl( 85 | lxcDXPServerProtocol + "://" + lxcDXPMainDomain 86 | ).defaultHeader( 87 | HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE 88 | ).defaultHeader( 89 | HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE 90 | ).build(); 91 | 92 | webClient.patch( 93 | ).uri( 94 | "/o/c/tickets/{ticketId}", jsonTicketDTO.getLong("id") 95 | ).bodyValue( 96 | jsonProperties.toString() 97 | ).header( 98 | HttpHeaders.AUTHORIZATION, "Bearer " + jwtToken 99 | ).exchangeToMono( 100 | clientResponse -> { 101 | HttpStatus httpStatus = clientResponse.statusCode(); 102 | 103 | if (httpStatus.is2xxSuccessful()) { 104 | return clientResponse.bodyToMono(String.class); 105 | } 106 | else if (httpStatus.is4xxClientError()) { 107 | if (_log.isInfoEnabled()) { 108 | _log.info("Output: " + httpStatus.getReasonPhrase()); 109 | } 110 | } 111 | 112 | Mono mono = 113 | clientResponse.createException(); 114 | 115 | return mono.flatMap(Mono::error); 116 | } 117 | ).retryWhen( 118 | Retry.backoff( 119 | 3, Duration.ofSeconds(1) 120 | ).doAfterRetry( 121 | retrySignal -> _log.info("Retrying request") 122 | ) 123 | ).doOnNext( 124 | output -> { 125 | if (_log.isInfoEnabled()) { 126 | _log.info("Output: " + output); 127 | } 128 | } 129 | ).subscribe(); 130 | } 131 | 132 | private String _getSuggestionsJSON(String subject) { 133 | JSONArray suggestionsJSONArray = new JSONArray(); 134 | 135 | SuggestionsContributorConfiguration 136 | suggestionsContributorConfiguration = 137 | new SuggestionsContributorConfiguration(); 138 | 139 | suggestionsContributorConfiguration.setContributorName("sxpBlueprint"); 140 | 141 | suggestionsContributorConfiguration.setDisplayGroupName( 142 | "Public Nav Search Recommendations"); 143 | 144 | suggestionsContributorConfiguration.setSize(3); 145 | 146 | JSONObject attributes = new JSONObject(); 147 | 148 | attributes.put( 149 | "includeAssetSearchSummary", true 150 | ).put( 151 | "includeassetURL", true 152 | ).put( 153 | "sxpBlueprintId", 3628599 154 | ); 155 | 156 | suggestionsContributorConfiguration.setAttributes(attributes); 157 | 158 | try { 159 | Page 160 | suggestionsContributorResultsPage = 161 | _suggestionResource.postSuggestionsPage( 162 | SUGGESTION_SCHEME + "://" + SUGGESTION_HOST, "/search", 163 | 3190049L, "", 1434L, "this-site", subject, 164 | new SuggestionsContributorConfiguration[] { 165 | suggestionsContributorConfiguration 166 | }); 167 | 168 | for (SuggestionsContributorResults suggestionsContributorResults : 169 | suggestionsContributorResultsPage.getItems()) { 170 | 171 | Suggestion[] suggestions = 172 | suggestionsContributorResults.getSuggestions(); 173 | 174 | for (Suggestion suggestion : suggestions) { 175 | String text = suggestion.getText(); 176 | 177 | JSONObject suggestionAttributes = new JSONObject( 178 | String.valueOf(suggestion.getAttributes())); 179 | 180 | String assetURL = (String)suggestionAttributes.get( 181 | "assetURL"); 182 | 183 | JSONObject suggestionJSONObject = new JSONObject(); 184 | 185 | suggestionJSONObject.put( 186 | "assetURL", 187 | SUGGESTION_SCHEME + "://" + SUGGESTION_HOST + assetURL 188 | ).put( 189 | "text", text 190 | ); 191 | suggestionsJSONArray.put(suggestionJSONObject); 192 | } 193 | } 194 | } 195 | catch (Exception exception) { 196 | if (_log.isErrorEnabled()) { 197 | _log.error("Could not retrieve search suggestions", exception); 198 | } 199 | 200 | // Always return something for the purposes of the workshop 201 | 202 | JSONObject suggestionJSONObject = new JSONObject(); 203 | 204 | suggestionJSONObject.put( 205 | "assetURL", SUGGESTION_SCHEME + "://" + SUGGESTION_HOST 206 | ).put( 207 | "text", "learn.liferay.com" 208 | ); 209 | suggestionsJSONArray.put(suggestionJSONObject); 210 | } 211 | 212 | return suggestionsJSONArray.toString(); 213 | } 214 | 215 | private void _initResourceBuilders() { 216 | SuggestionResource.Builder dataDefinitionResourceBuilder = 217 | SuggestionResource.builder(); 218 | 219 | _suggestionResource = dataDefinitionResourceBuilder.header( 220 | HttpHeaders.USER_AGENT, TicketRestController.class.getName() 221 | ).header( 222 | HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE 223 | ).header( 224 | HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE 225 | ).endpoint( 226 | SUGGESTION_HOST, SUGGESTION_PORT, SUGGESTION_SCHEME 227 | ).build(); 228 | } 229 | 230 | private static final Log _log = LogFactory.getLog( 231 | DocumentationReferral.class); 232 | 233 | private SuggestionResource _suggestionResource; 234 | 235 | } -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/src/main/java/com/liferay/ticket/TicketRestController.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | package com.liferay.ticket; 16 | 17 | import org.apache.commons.logging.Log; 18 | import org.apache.commons.logging.LogFactory; 19 | 20 | import org.json.JSONObject; 21 | 22 | import org.springframework.beans.factory.annotation.Value; 23 | import org.springframework.http.HttpStatus; 24 | import org.springframework.http.ResponseEntity; 25 | import org.springframework.security.core.annotation.AuthenticationPrincipal; 26 | import org.springframework.security.oauth2.jwt.Jwt; 27 | import org.springframework.web.bind.annotation.GetMapping; 28 | import org.springframework.web.bind.annotation.PostMapping; 29 | import org.springframework.web.bind.annotation.RequestBody; 30 | import org.springframework.web.bind.annotation.RestController; 31 | 32 | /** 33 | * @author Raymond Augé 34 | * @author Gregory Amerson 35 | * @author Allen Ziegenfus 36 | */ 37 | @RestController 38 | public class TicketRestController { 39 | 40 | @GetMapping("/ready") 41 | public String getReady() { 42 | return "READY"; 43 | } 44 | 45 | @PostMapping("/ticket/object/action/documentation/referral") 46 | public ResponseEntity postTicketObjectAction1( 47 | @AuthenticationPrincipal Jwt jwt, @RequestBody String json) { 48 | 49 | if (_log.isInfoEnabled()) { 50 | _log.info("JWT Claims: " + jwt.getClaims()); 51 | _log.info("JWT ID: " + jwt.getId()); 52 | _log.info("JWT Subject: " + jwt.getSubject()); 53 | } 54 | 55 | try { 56 | JSONObject jsonObject = new JSONObject(json); 57 | 58 | if (_log.isInfoEnabled()) { 59 | _log.info("JSON INPUT: \n\n" + jsonObject.toString(4) + "\n"); 60 | } 61 | 62 | _documentationReferral.addDocumentationReferralAndQueue( 63 | _lxcDXPServerProtocol, _lxcDXPMainDomain, 64 | jwt.getTokenValue(), jsonObject); 65 | } 66 | catch (Exception exception) { 67 | _log.error("JSON: " + json, exception); 68 | } 69 | 70 | return new ResponseEntity<>(json, HttpStatus.CREATED); 71 | } 72 | 73 | private static final Log _log = LogFactory.getLog( 74 | TicketRestController.class); 75 | 76 | private DocumentationReferral _documentationReferral = 77 | new DocumentationReferral(); 78 | 79 | @Value("${com.liferay.lxc.dxp.mainDomain}") 80 | private String _lxcDXPMainDomain; 81 | 82 | @Value("${com.liferay.lxc.dxp.server.protocol}") 83 | private String _lxcDXPServerProtocol; 84 | 85 | } -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/src/main/java/com/liferay/ticket/TicketSpringBootApplication.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 3 | * 4 | * This library is free software; you can redistribute it and/or modify it under 5 | * the terms of the GNU Lesser General Public License as published by the Free 6 | * Software Foundation; either version 2.1 of the License, or (at your option) 7 | * any later version. 8 | * 9 | * This library is distributed in the hope that it will be useful, but WITHOUT 10 | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 11 | * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 12 | * details. 13 | */ 14 | 15 | package com.liferay.ticket; 16 | 17 | import com.liferay.client.extension.util.spring.boot.ClientExtensionUtilSpringBootComponentScan; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.context.annotation.Import; 22 | 23 | /** 24 | * @author Raymond Augé 25 | * @author Gregory Amerson 26 | * @author Brian Wing Shun Chan 27 | */ 28 | @Import(ClientExtensionUtilSpringBootComponentScan.class) 29 | @SpringBootApplication 30 | public class TicketSpringBootApplication { 31 | 32 | public static void main(String[] args) { 33 | SpringApplication.run(TicketSpringBootApplication.class, args); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/src/main/resources/application-default.properties: -------------------------------------------------------------------------------- 1 | # 2 | # OAuth 3 | # 4 | 5 | liferay.oauth.application.external.reference.codes=ticket-spring-boot-oauth-application-user-agent 6 | liferay.oauth.urls.excludes=/ready 7 | 8 | # 9 | # Spring Boot 10 | # 11 | 12 | server.port=58081 -------------------------------------------------------------------------------- /client-extensions/ticket-spring-boot/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.config.import=\ 2 | classpath:/application-default.properties,\ 3 | optional:configtree:${LIFERAY_ROUTES_CLIENT_EXTENSION}/,\ 4 | optional:configtree:${LIFERAY_ROUTES_DXP}/ -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/README.markdown: -------------------------------------------------------------------------------- 1 | # Live editing ticket-theme-css 2 | 3 | By deploying the dev version of this client extension, the build is changed as follows: 4 | 5 | - A custom gulpfile.mjs watches the .scss files in `src` and builds them using Dart SASS 6 | - A javascript extension is deployed which enables Browsersync to run in Liferay 7 | - When updates are made, Browsersync is notified and the css is updated without having to explicitly deploy or refresh the browser. 8 | 9 | ## Example: 10 | 11 | ![Example](./liveedit.gif) 12 | 13 | ## Limitations: 14 | 15 | - Compiling clay is relatively slow (4-5 seconds) so be patient. But main.css changes are updated relatively quickly. 16 | - This is a custom build that might not 100% reflect the normal gradle build 17 | 18 | ## Setup: 19 | 20 | 1. Deploy in dev mode: 21 | 22 | `../../gradlew clean deployDev packageRunServe` 23 | 24 | 2. Enable the Browsersync javascript extension. It is named "Ticket Theme Live JS" and needs to be added to whatever context you are using to test (e.g. you can add it to all pages or just the page you are testing) 25 | 26 | 3. Update scss files - the css changes should show automatically -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/client-extension.dev.yaml: -------------------------------------------------------------------------------- 1 | ticket-theme-css: 2 | clayURL: http://localhost:3000/css/clay.css 3 | mainURL: http://localhost:3000/css/main.css 4 | ticket-theme-live-js: 5 | name: Ticket Theme Live JS 6 | type: globalJS 7 | url: http://localhost:3000/browser-sync/browser-sync-client.js -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/client-extension.yaml: -------------------------------------------------------------------------------- 1 | assemble: 2 | - from: build/buildTheme/img 3 | into: static/img 4 | ticket-theme-css: 5 | clayURL: css/clay.css 6 | mainURL: css/main.css 7 | name: Ticket Theme CSS 8 | type: themeCSS -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/gulpfile.mjs: -------------------------------------------------------------------------------- 1 | import browserSync from 'browser-sync'; 2 | import gulp from 'gulp'; 3 | import gulpSass from 'gulp-sass'; 4 | import sass from 'sass-embedded'; 5 | 6 | const browserSyncInstance = browserSync.create(); 7 | const gulpSassInstance = gulpSass(sass); 8 | const themeBuildDir = './build/buildTheme'; 9 | 10 | gulp.task('copy:html', () => { 11 | return gulp.src('index.html').pipe(gulp.dest(themeBuildDir)); 12 | }); 13 | 14 | gulp.task('copy:src', () => { 15 | return gulp.src('src/**/*').pipe(gulp.dest(themeBuildDir)); 16 | }); 17 | 18 | function compileSass(cssFile) { 19 | return gulp 20 | .src(cssFile) 21 | .pipe( 22 | gulpSassInstance({ 23 | includePaths: [ 24 | 'node_modules', 25 | 'node_modules/bourbon/core', 26 | '../../node_modules/bourbon/core', 27 | ], 28 | }).on('error', gulpSassInstance.logError) 29 | ) 30 | .pipe(gulp.dest(`${themeBuildDir}/css`)) 31 | .pipe(browserSyncInstance.stream()); 32 | } 33 | 34 | gulp.task( 35 | 'clay:css', 36 | gulp.series('copy:src', () => compileSass(`${themeBuildDir}/css/clay.scss`)) 37 | ); 38 | 39 | gulp.task( 40 | 'main:css', 41 | gulp.series('copy:src', () => compileSass(`${themeBuildDir}/css/main.scss`)) 42 | ); 43 | 44 | gulp.task( 45 | 'serve', 46 | gulp.series('copy:html', 'main:css', 'clay:css', () => { 47 | browserSyncInstance.init({ 48 | notify: false, 49 | open: false, 50 | server: themeBuildDir, 51 | socket: { 52 | domain: 'localhost:3000', 53 | }, 54 | }); 55 | 56 | gulp.watch('src/**/*', gulp.parallel('main:css', 'clay:css')); 57 | }) 58 | ); 59 | -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/liveedit.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiferayCloud/client-extensions-deep-dive-devcon-2023/6623bb959be830b7c2a1cc5eabe464235835b181/client-extensions/ticket-theme-css/liveedit.gif -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "sassy-inputs": "1.0.6" 4 | }, 5 | "devDependencies": { 6 | "bourbon": "^7.3.0", 7 | "browser-sync": "^2.29.1", 8 | "gulp": "^4.0.2", 9 | "gulp-sass": "^5.1.0", 10 | "sass-embedded": "^1.62.0" 11 | }, 12 | "liferayDesignPack": { 13 | "baseTheme": "styled" 14 | }, 15 | "main": "package.json", 16 | "name": "@liferay/tickets-theme-css", 17 | "scripts": { 18 | "serve": "gulp serve" 19 | }, 20 | "version": "1.0.0" 21 | } 22 | -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/src/css/_clay_variables.scss: -------------------------------------------------------------------------------- 1 | // Global Variables 2 | 3 | $clay-unset: clay-unset !default; 4 | 5 | // An alias for `$clay-unset` 6 | 7 | $c-unset: $clay-unset !default; 8 | 9 | $clay-unset-placeholder: clay-unset-placeholder !default; 10 | 11 | $enable-c-inner: true !default; 12 | $enable-lexicon-flat-colors: true !default; 13 | $enable-scaling-components: true !default; 14 | $scaling-breakpoint-down: sm !default; 15 | 16 | $enable-caret: false !default; 17 | $enable-deprecation-messages: true !default; 18 | $enable-gradients: false !default; 19 | $enable-grid-classes: true !default; 20 | $enable-pointer-cursor-for-buttons: true !default; 21 | $enable-prefers-reduced-motion-media-query: true !default; 22 | $enable-print-styles: true !default; 23 | $enable-responsive-font-sizes: false !default; 24 | $enable-rounded: true !default; 25 | $enable-shadows: true !default; 26 | $enable-transitions: true !default; 27 | $enable-validation-icons: true !default; 28 | 29 | // Deprecated, no longer affects any compiled CSS 30 | 31 | $enable-hover-media-query: false !default; 32 | 33 | // Theme Base Colors 34 | 35 | $black: #fff !default; 36 | $white: #000 !default; 37 | 38 | $gray-100: #272833 !default; 39 | $gray-200: #393a4a !default; 40 | $gray-300: #495057 !default; 41 | $gray-400: #6b6c7e !default; 42 | $gray-500: #a7a9bc !default; 43 | $gray-600: #cdced9 !default; 44 | $gray-700: #e7e7ed !default; 45 | $gray-800: #f1f2f5 !default; 46 | $gray-900: #f7f8f9 !default; 47 | 48 | $blue: if($enable-lexicon-flat-colors, #4b9fff, #0b5fff) !default; 49 | $cyan: if($enable-lexicon-flat-colors, #5fc8ff, #17a2b8) !default; 50 | $green: if($enable-lexicon-flat-colors, #9be169, #287d3d) !default; 51 | $indigo: if($enable-lexicon-flat-colors, #7785ff, #6610f2) !default; 52 | $orange: if($enable-lexicon-flat-colors, #ffb46e, #b95000) !default; 53 | $pink: if($enable-lexicon-flat-colors, #ff73c3, #e83e8c) !default; 54 | $purple: if($enable-lexicon-flat-colors, #af78ff, #6f42c1) !default; 55 | $red: if($enable-lexicon-flat-colors, #ff5f5f, #da1414) !default; 56 | $teal: if($enable-lexicon-flat-colors, #50d2a0, #20c997) !default; 57 | $yellow: if($enable-lexicon-flat-colors, #ffd76e, #ffc107) !default; 58 | 59 | $primary: #af84e1 !default; 60 | 61 | $primary-d1: darken($primary, 5.1) !default; 62 | $primary-d2: darken($primary, 10) !default; 63 | $primary-l1: lighten($primary, 22.94) !default; 64 | $primary-l2: lighten($primary, 32.94) !default; 65 | $primary-l3: lighten($primary, 44.9) !default; 66 | 67 | $secondary: #5e72a5 !default; 68 | 69 | $secondary-d1: darken(saturate($secondary, 4.82), 20) !default; 70 | $secondary-d2: darken(saturate($secondary, 5.36), 23.92) !default; 71 | $secondary-l1: lighten( 72 | saturate(adjust-hue($secondary, -3), 5.39), 73 | 23.92 74 | ) !default; 75 | $secondary-l2: lighten( 76 | saturate(adjust-hue($secondary, -2), 5.48), 77 | 37.06 78 | ) !default; 79 | $secondary-l3: lighten( 80 | saturate(adjust-hue($secondary, 3), 6.13), 81 | 46.08 82 | ) !default; 83 | 84 | $info: #2e5aac !default; 85 | 86 | $info-d1: darken($info, 5) !default; 87 | $info-d2: darken($info, 10) !default; 88 | $info-l1: lighten(saturate($info, 0.59), 28.04) !default; 89 | $info-l2: lighten(desaturate($info, 3.25), 52.94) !default; 90 | 91 | $success: #287d3c !default; 92 | 93 | $success-d1: darken($success, 5) !default; 94 | $success-d2: darken($success, 10) !default; 95 | $success-l1: lighten(desaturate($success, 0.14), 24.95) !default; 96 | $success-l2: lighten(desaturate($success, 1.52), 62.94) !default; 97 | 98 | $warning: #b95000 !default; 99 | 100 | $warning-d1: darken($warning, 5.1) !default; 101 | $warning-d2: darken($warning, 10) !default; 102 | $warning-l1: lighten($warning, 24.9) !default; 103 | $warning-l2: lighten($warning, 60) !default; 104 | 105 | $danger: #da1414 !default; 106 | 107 | $danger-d1: darken($danger, 5) !default; 108 | $danger-d2: darken($danger, 10) !default; 109 | $danger-l1: lighten(desaturate($danger, 0.25), 28.04) !default; 110 | $danger-l2: lighten(saturate($danger, 5.04), 50) !default; 111 | 112 | $light: #272833 !default; 113 | 114 | $light-d1: darken($light, 5.1) !default; 115 | $light-d2: darken($light, 10) !default; 116 | $light-l1: lighten(desaturate(adjust-hue($light, -15), 2.38), 1.96) !default; 117 | $light-l2: lighten(desaturate(adjust-hue($light, -225), 16.67), 4.71) !default; 118 | 119 | $dark: #f1f2f5 !default; 120 | 121 | $dark-d1: darken($dark, 5.1) !default; 122 | $dark-d2: darken($dark, 10) !default; 123 | $dark-l1: lighten(saturate($dark, 0.18), 4.12) !default; 124 | $dark-l2: lighten(desaturate($dark, 0.36), 8.04) !default; 125 | 126 | $theme-colors: () !default; 127 | $theme-colors: map-merge( 128 | ( 129 | 'danger': $danger, 130 | 'dark': $dark, 131 | 'info': $info, 132 | 'light': $light, 133 | 'primary': $primary, 134 | 'secondary': $secondary, 135 | 'success': $success, 136 | 'warning': $warning, 137 | ), 138 | $theme-colors 139 | ); 140 | 141 | // Set a specific jump point for requesting color jumps 142 | 143 | $theme-color-interval: 8% !default; 144 | 145 | // The yiq lightness value that determines when the lightness of color changes from "dark" to "light". Acceptable values are between 0 and 255. 146 | 147 | $yiq-contrasted-threshold: 150 !default; 148 | 149 | $yiq-text-dark: $gray-900 !default; 150 | $yiq-text-light: $white !default; 151 | 152 | // Spacing 153 | 154 | $spacer: 1rem !default; 155 | $spacers: () !default; 156 | $spacers: map-deep-merge( 157 | ( 158 | 0: var(--spacer-0, 0), 159 | 1: var(--spacer-1, $spacer * 0.25), 160 | 2: var(--spacer-2, $spacer * 0.5), 161 | 3: var(--spacer-3, $spacer * 1), 162 | 4: var(--spacer-4, $spacer * 1.5), 163 | 5: var(--spacer-5, $spacer * 3), 164 | 6: var(--spacer-6, $spacer * 4.5), 165 | 7: var(--spacer-7, $spacer * 6), 166 | 8: var(--spacer-8, $spacer * 7.5), 167 | 9: var(--spacer-9, $spacer * 9), 168 | 10: var(--spacer-10, $spacer * 10), 169 | ), 170 | $spacers 171 | ); 172 | 173 | $moz-osx-font-smoothing: grayscale !default; 174 | $webkit-font-smoothing: antialiased !default; 175 | 176 | $font-import-url: null !default; 177 | 178 | $font-family-monospace: sfmono-regular, menlo, monaco, consolas, 179 | 'Liberation Mono', 'Courier New', monospace !default; 180 | $font-family-sans-serif: system-ui, -apple-system, blinkmacsystemfont, 181 | 'Segoe UI', roboto, oxygen-sans, ubuntu, cantarell, 'Helvetica Neue', arial, 182 | sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol' !default; 183 | $font-family-serif: georgia, 'Times New Roman', times, serif !default; 184 | 185 | $font-family-base: var(--font-family-base, $font-family-sans-serif) !default; 186 | 187 | $font-size-base: 1rem !default; 188 | 189 | $font-size-lg: 1.125rem !default; 190 | $font-size-sm: 0.875rem !default; 191 | 192 | $font-size-base-mobile: $font-size-base !default; 193 | $font-size-lg-mobile: $font-size-lg !default; 194 | 195 | $font-weight-bold: var(--font-weight-bold, 700) !default; 196 | $font-weight-bolder: var(--font-weight-bolder, 900) !default; 197 | $font-weight-light: var(--font-weight-light, 300) !default; 198 | $font-weight-lighter: var(--font-weight-lighter, lighter) !default; 199 | $font-weight-normal: var(--font-weight-normal, 400) !default; 200 | $font-weight-semi-bold: var(--font-weight-semi-bold, 600) !default; 201 | 202 | $font-weight-base: $font-weight-normal !default; 203 | 204 | $h1-font-size: var(--h1-font-size, 1.625rem) !default; 205 | $h2-font-size: var(--h2-font-size, 1.375rem) !default; 206 | $h3-font-size: var(--h3-font-size, 1.1875rem) !default; 207 | $h4-font-size: var(--h4-font-size, 1rem) !default; 208 | $h5-font-size: var(--h5-font-size, 0.875rem) !default; 209 | $h6-font-size: var(--h6-font-size, 0.8125rem) !default; 210 | 211 | $h1-font-size-mobile: null !default; 212 | $h2-font-size-mobile: null !default; 213 | $h3-font-size-mobile: null !default; 214 | $h4-font-size-mobile: null !default; 215 | $h5-font-size-mobile: null !default; 216 | $h6-font-size-mobile: null !default; 217 | 218 | $headings-color: null !default; 219 | $headings-font-family: null !default; 220 | $headings-font-weight: $font-weight-bold !default; 221 | $headings-line-height: 1.2 !default; 222 | $headings-margin-bottom: $spacer * 0.5 !default; 223 | 224 | current-tickets-custom-element section main .rdg { 225 | --rdg-color-scheme: dark; 226 | --rdg-background-color: #000; 227 | --rdg-color: #fff; 228 | --rdg-border-color: #4c5672; 229 | --rdg-summary-border-color: #4c5672; 230 | --rdg-background-color: hsl(0deg 100% 0%); 231 | --rdg-header-background-color: hsl(0deg 97.5% 0%); 232 | --rdg-row-hover-background-color: hsl(0deg 96% 0%); 233 | --rdg-row-selected-background-color: hsl(207deg 92% 76%); 234 | --rdg-row-selected-hover-background-color: hsl(207deg 88% 76%); 235 | --rdg-checkbox-color: hsl(207deg 100% 29%); 236 | --rdg-checkbox-focus-color: hsl(207deg 100% 69%); 237 | --rdg-checkbox-disabled-border-color: #ccc; 238 | --rdg-checkbox-disabled-background-color: #ddd; 239 | --rdg-selection-color: #66afe9; 240 | --rdg-font-size: 14px; 241 | } -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/src/css/_custom.scss: -------------------------------------------------------------------------------- 1 | /* This is a hack to switch out a logo - just to show what is possible with themes */ 2 | 3 | .logo img, .logo h1 { 4 | display: none; 5 | } 6 | 7 | .logo:before { 8 | content: url("../img/logo.svg"); 9 | } 10 | 11 | .logo:after { 12 | content: "Ticket CENTER"; 13 | font-size: 2rem; 14 | margin-left: 1rem; 15 | } 16 | 17 | #wrapper > footer, #wrapper > header { 18 | border: none; 19 | } 20 | 21 | nav.bg-dark { 22 | background-color: black !important; 23 | border: none; 24 | } -------------------------------------------------------------------------------- /client-extensions/ticket-theme-css/src/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /configs/local/portal-ext.properties: -------------------------------------------------------------------------------- 1 | # 2 | # MySQL 3 | # 4 | #jdbc.default.driverClassName=com.mysql.cj.jdbc.Driver 5 | #jdbc.default.url=jdbc:mysql://localhost/lportal?useUnicode=true&characterEncoding=UTF-8&useFastDateParsing=false 6 | #jdbc.default.username=root 7 | #jdbc.default.password= 8 | 9 | # 10 | # Feature flags 11 | # 12 | 13 | # Enable the import/export center 14 | feature.flag.COMMERCE-8087=true 15 | 16 | # 17 | # General 18 | # 19 | 20 | passwords.default.policy.change.required=false 21 | setup.wizard.enabled=false 22 | terms.of.use.required=false 23 | users.reminder.queries.enabled=false 24 | cache.filter.include.user.agent=false 25 | com.liferay.portal.servlet.filters.cache.CacheFilter=false 26 | session.timeout=120 -------------------------------------------------------------------------------- /configs/local/tomcat/webapps/ROOT/WEB-INF/classes/META-INF/portal-log4j-ext.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /configs/local/tomcat/webapps/ROOT/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | axis.servicesPath 6 | /api/axis/ 7 | 8 | 9 | contextClass 10 | com.liferay.portal.spring.context.PortalApplicationContext 11 | 12 | 13 | contextConfigLocation 14 | 15 | 16 | 17 | com.ibm.websphere.portletcontainer.PortletDeploymentEnabled 18 | false 19 | 20 | 21 | osgi.http.endpoint 22 | /o/ 23 | 24 | 25 | 120 26 | 27 | true 28 | 29 | 30 | 31 | js 32 | text/javascript 33 | 34 | 35 | png 36 | image/png 37 | 38 | 39 | xml 40 | text/xml 41 | 42 | 43 | index.html 44 | index.jsp 45 | 46 | 47 | /errors/code.jsp 48 | 49 | 50 | 51 | /c/portal/protected 52 | /c/portal/protected 53 | /ar/c/portal/protected 54 | /ar-SA/c/portal/protected 55 | /ar_SA/c/portal/protected 56 | /bg/c/portal/protected 57 | /bg-BG/c/portal/protected 58 | /bg_BG/c/portal/protected 59 | /ca/c/portal/protected 60 | /ca-AD/c/portal/protected 61 | /ca-ES/c/portal/protected 62 | /ca-ES-VALENCIA/c/portal/protected 63 | /ca_AD/c/portal/protected 64 | /ca_ES/c/portal/protected 65 | /ca_ES_VALENCIA/c/portal/protected 66 | /cs/c/portal/protected 67 | /cs-CZ/c/portal/protected 68 | /cs_CZ/c/portal/protected 69 | /da/c/portal/protected 70 | /da-DK/c/portal/protected 71 | /da_DK/c/portal/protected 72 | /de/c/portal/protected 73 | /de-DE/c/portal/protected 74 | /de_DE/c/portal/protected 75 | /el/c/portal/protected 76 | /el-GR/c/portal/protected 77 | /el_GR/c/portal/protected 78 | /en/c/portal/protected 79 | /en-AU/c/portal/protected 80 | /en-GB/c/portal/protected 81 | /en-US/c/portal/protected 82 | /en_AU/c/portal/protected 83 | /en_GB/c/portal/protected 84 | /en_US/c/portal/protected 85 | /es/c/portal/protected 86 | /es-AR/c/portal/protected 87 | /es-CO/c/portal/protected 88 | /es-ES/c/portal/protected 89 | /es-MX/c/portal/protected 90 | /es_AR/c/portal/protected 91 | /es_CO/c/portal/protected 92 | /es_ES/c/portal/protected 93 | /es_MX/c/portal/protected 94 | /et/c/portal/protected 95 | /et-EE/c/portal/protected 96 | /et_EE/c/portal/protected 97 | /eu/c/portal/protected 98 | /eu-ES/c/portal/protected 99 | /eu_ES/c/portal/protected 100 | /fa/c/portal/protected 101 | /fa-IR/c/portal/protected 102 | /fa_IR/c/portal/protected 103 | /fi/c/portal/protected 104 | /fi-FI/c/portal/protected 105 | /fi_FI/c/portal/protected 106 | /fr/c/portal/protected 107 | /fr-CA/c/portal/protected 108 | /fr-FR/c/portal/protected 109 | /fr_CA/c/portal/protected 110 | /fr_FR/c/portal/protected 111 | /gl/c/portal/protected 112 | /gl-ES/c/portal/protected 113 | /gl_ES/c/portal/protected 114 | /hi/c/portal/protected 115 | /hi-IN/c/portal/protected 116 | /hi_IN/c/portal/protected 117 | /hr/c/portal/protected 118 | /hr-HR/c/portal/protected 119 | /hr_HR/c/portal/protected 120 | /hu/c/portal/protected 121 | /hu-HU/c/portal/protected 122 | /hu_HU/c/portal/protected 123 | /in/c/portal/protected 124 | /in-ID/c/portal/protected 125 | /in_ID/c/portal/protected 126 | /it/c/portal/protected 127 | /it-IT/c/portal/protected 128 | /it_IT/c/portal/protected 129 | /iw/c/portal/protected 130 | /iw-IL/c/portal/protected 131 | /iw_IL/c/portal/protected 132 | /ja/c/portal/protected 133 | /ja-JP/c/portal/protected 134 | /ja_JP/c/portal/protected 135 | /kk/c/portal/protected 136 | /kk-KZ/c/portal/protected 137 | /kk_KZ/c/portal/protected 138 | /km/c/portal/protected 139 | /km-KH/c/portal/protected 140 | /km_KH/c/portal/protected 141 | /ko/c/portal/protected 142 | /ko-KR/c/portal/protected 143 | /ko_KR/c/portal/protected 144 | /lo/c/portal/protected 145 | /lo-LA/c/portal/protected 146 | /lo_LA/c/portal/protected 147 | /lt/c/portal/protected 148 | /lt-LT/c/portal/protected 149 | /lt_LT/c/portal/protected 150 | /ms/c/portal/protected 151 | /ms-MY/c/portal/protected 152 | /ms_MY/c/portal/protected 153 | /nb/c/portal/protected 154 | /nb-NO/c/portal/protected 155 | /nb_NO/c/portal/protected 156 | /nl/c/portal/protected 157 | /nl-BE/c/portal/protected 158 | /nl-NL/c/portal/protected 159 | /nl_BE/c/portal/protected 160 | /nl_NL/c/portal/protected 161 | /pl/c/portal/protected 162 | /pl-PL/c/portal/protected 163 | /pl_PL/c/portal/protected 164 | /pt/c/portal/protected 165 | /pt-BR/c/portal/protected 166 | /pt-PT/c/portal/protected 167 | /pt_BR/c/portal/protected 168 | /pt_PT/c/portal/protected 169 | /ro/c/portal/protected 170 | /ro-RO/c/portal/protected 171 | /ro_RO/c/portal/protected 172 | /ru/c/portal/protected 173 | /ru-RU/c/portal/protected 174 | /ru_RU/c/portal/protected 175 | /sk/c/portal/protected 176 | /sk-SK/c/portal/protected 177 | /sk_SK/c/portal/protected 178 | /sl/c/portal/protected 179 | /sl-SI/c/portal/protected 180 | /sl_SI/c/portal/protected 181 | /sr/c/portal/protected 182 | /sr-RS/c/portal/protected 183 | /sr-RS-latin/c/portal/protected 184 | /sr_RS/c/portal/protected 185 | /sr_RS_latin/c/portal/protected 186 | /sv/c/portal/protected 187 | /sv-SE/c/portal/protected 188 | /sv_SE/c/portal/protected 189 | /ta/c/portal/protected 190 | /ta-IN/c/portal/protected 191 | /ta_IN/c/portal/protected 192 | /th/c/portal/protected 193 | /th-TH/c/portal/protected 194 | /th_TH/c/portal/protected 195 | /tr/c/portal/protected 196 | /tr-TR/c/portal/protected 197 | /tr_TR/c/portal/protected 198 | /uk/c/portal/protected 199 | /uk-UA/c/portal/protected 200 | /uk_UA/c/portal/protected 201 | /vi/c/portal/protected 202 | /vi-VN/c/portal/protected 203 | /vi_VN/c/portal/protected 204 | /zh/c/portal/protected 205 | /zh-CN/c/portal/protected 206 | /zh-TW/c/portal/protected 207 | /zh_CN/c/portal/protected 208 | /zh_TW/c/portal/protected 209 | 210 | 211 | users 212 | 213 | 214 | NONE 215 | 216 | 217 | 218 | FORM 219 | PortalRealm 220 | 221 | /c/portal/j_login 222 | /c/portal/j_login_error 223 | 224 | 225 | 226 | users 227 | 228 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | ## 2 | ## Check GETTING_STARTED.markdown for additional properties and their default 3 | ## values. 4 | ## 5 | 6 | liferay.marketplace.prefix=J3Y7 7 | liferay.workspace.bundle.dist.include.metadata=true 8 | liferay.workspace.modules.dir=modules 9 | liferay.workspace.node.package.manager=yarn 10 | liferay.workspace.product=dxp-7.4-u92 11 | liferay.workspace.themes.dir=themes 12 | liferay.workspace.wars.dir=modules 13 | microsoft.translator.subscription.key= -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiferayCloud/client-extensions-deep-dive-devcon-2023/6623bb959be830b7c2a1cc5eabe464235835b181/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.9.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /img/application-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiferayCloud/client-extensions-deep-dive-devcon-2023/6623bb959be830b7c2a1cc5eabe464235835b181/img/application-screenshot.png -------------------------------------------------------------------------------- /img/apply-theme.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiferayCloud/client-extensions-deep-dive-devcon-2023/6623bb959be830b7c2a1cc5eabe464235835b181/img/apply-theme.gif -------------------------------------------------------------------------------- /img/edit-home-page.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiferayCloud/client-extensions-deep-dive-devcon-2023/6623bb959be830b7c2a1cc5eabe464235835b181/img/edit-home-page.gif -------------------------------------------------------------------------------- /img/lcp-console-network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiferayCloud/client-extensions-deep-dive-devcon-2023/6623bb959be830b7c2a1cc5eabe464235835b181/img/lcp-console-network.png -------------------------------------------------------------------------------- /img/ticket-attributes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LiferayCloud/client-extensions-deep-dive-devcon-2023/6623bb959be830b7c2a1cc5eabe464235835b181/img/ticket-attributes.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": { 4 | "packages": [ 5 | "client-extensions/current-tickets-custom-element", 6 | "client-extensions/ticket-theme-css", 7 | "client-extensions/ticket-etc-node" 8 | ] 9 | } 10 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | dependencies { 3 | classpath group: "biz.aQute.bnd", name: "biz.aQute.bnd.gradle", version: "5.3.0" 4 | classpath group: "com.liferay", name: "com.liferay.gradle.plugins.workspace", version: "9.0.4" 5 | } 6 | 7 | repositories { 8 | mavenLocal() 9 | 10 | maven { 11 | url new File(rootProject.projectDir, "../../.m2-tmp") 12 | } 13 | 14 | maven { 15 | url "https://repository-cdn.liferay.com/nexus/content/groups/public" 16 | } 17 | } 18 | } 19 | 20 | apply plugin: "com.liferay.workspace" --------------------------------------------------------------------------------