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 | 
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 | 
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 | 
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 | 
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 | 
--------------------------------------------------------------------------------
/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 |