Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LPS-139246 - Create custom autocomplete for object-dynamic-data-mapping #1471

Closed

Conversation

bryceosterhaus
Copy link
Collaborator

https://issues.liferay.com/browse/LPS-139246

This refactors the autocomplete component to be used outside of commerce. I wasn't able to test this in the DXP UI due to backend errors with commerce, but thought I would get this up for review and then test.

I'll leave some comments about parts I moved around

@liferay-continuous-integration
Copy link
Collaborator

Please only forward necessary changes to Brian Chan during stabilization. Nonurgent changes should wait until the ongoing DXP 7.4 GA1 and Portal 7.4 GA4 release has been completed. For more details on the release timeline and status, see product-delivery.

@liferay-continuous-integration
Copy link
Collaborator

CI is automatically triggering the following test suites:

  •     ci:test:relevant
  •     ci:test:sf

@liferay-continuous-integration
Copy link
Collaborator

❌ ci:test:sf - 0 out of 1 jobs passed in 2 minutes

Click here for more details.

Base Branch:

Branch Name: master
Branch GIT ID: 77d84cd9f7f6c106d9a42bdee06a57003211ee91

Sender Branch:

Branch Name: LPS-139246
Branch GIT ID: 1d9c3cede8967cae917ef47d68a696179c9c0c51

0 out of 1jobs PASSED
For more details click here.
     [exec] Configuration on demand is an incubating feature.
     [exec] > Task :downloadNode UP-TO-DATE
     [exec] > Task :npmInstall SKIPPED
     [exec] 
     [exec] > Task :yarnInstall
     [exec] yarn install v1.13.0
     [exec] [1/4] Resolving packages...
     [exec] success Already up-to-date.
     [exec] Done in 1.01s.
     [exec] 
     [exec] > Task :packageRunCheckFormat
     [exec] yarn run v1.13.0
     [exec] \$ liferay-npm-scripts check
     [exec] Preflight check failed:
     [exec] apps/object/object-dynamic-data-mapping/package.json: BAD - package name object-dynamic-data-mapping should be under @liferay/ named scope - https://git.io/JOgy7
     [exec] 
     [exec] Prettier checked 5 files
     [exec] 
     [exec] 1 of 3 jobs failed
     [exec] info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
     [exec] error Command failed with exit code 1.
     [exec] 
     [exec] > Task :packageRunCheckFormat FAILED
     [exec] Gradle build finished at 2021-09-15 22:33:27.632.
     [exec] 3 actionable tasks: 2 executed, 1 up-to-date
     [exec] 
     [exec] See the profiling report at: file:///opt/dev/projects/github/liferay-portal/build/reports/profile/profile-2021-09-15-15-33-17.html
     [exec] A fine-grained performance profile is available: use the --scan option.
     [exec] 
     [exec] 
     [exec] FAILURE: Build failed with an exception.
     [exec] 
     [exec] * What went wrong:
     [exec] Execution failed for task ':packageRunCheckFormat'.
     [exec] > Process 'command '/opt/dev/projects/github/liferay-portal/build/node/bin/node'' finished with non-zero exit value 1
     [exec] 
     [exec] * Try:
     [exec] Run with --info or --debug option to get more log output. Run with --scan to get full insights.
     [exec] 
     [exec] * Exception is:
     [exec] org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':packageRunCheckFormat'.
     [exec] 	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda\$executeIfValid\$1(ExecuteActionsTaskExecuter.java:208)
     [exec] 	at org.gradle.internal.Try\$Failure.ifSuccessfulOrElse(Try.java:263)
     [exec] 	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:206)
     [exec] 	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:187)

@liferay-continuous-integration
Copy link
Collaborator

Comment on lines -93 to -95
<li>
<div ref={ordersListRef} />
</li>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a very strange way to do this. I need to properly test this in the UI but I really don't like this pattern being used. Effectively this was an anti-pattern here of bi-directional data flow. If the Autocomplete component was passed a customView prop, instead of rendering inside of the Autocomplete component it was handling all the business logic of requesting items and then rendering them via createPortal to this node. The reason this pattern is bad is because Autocomplete is now rendering to an unrelated area. If we want to properly do this, we must pull the state(items) up the react tree and outside of Autocomplete. That way we either pass the items to Autocomplete or we render them here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @FabioDiegoMastrorilli

I noticed you added this API in liferay@4aa3853#diff-fbc58da25a23f789ae15c90776dd7b5ce184557ea3d17369a143dfb8bb7554bcR270

Can you verify my changes in this PR and see how we can get around this pattern of using the portal to render the content on an outside node.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @bryceosterhaus it makes totally sense :)
Passing a ref there would help in case we would like to use all the autocomplete functionalities (search, infinite scrolling, custom views) to render a list of results even out of a react context. In that case there won't be any state tree so it wont't look like an unsafe operation.

That being said... please have a look at the AccountSelector component. I've use the Autocomplete there to avoid code duplication and, if I could still use it there to get the same result, it would be great.

const node = useRef();
const dropdownNode = useRef();
const inputNode = useRef();
const FetchedCustomView = useLiferayModule(props.customViewModuleUrl);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed this part and the customViewModuleUrl prop because I didn't see this being used anywhere

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was a requirement from @marco-leo to let external developer using their own views by passing their module url.
We do something similar here - I created the hook useLiferayModule during the migration so it's not used there, I think it can be moved out of commerce modules for better reusability.

We don't use it internally but somebody theoretically could.

NB:
I just realised we have the same useLiferayModule function declared twice 🤦

@bryceosterhaus
Copy link
Collaborator Author

cc @marco-leo, I think you originally authored this Autocomplete component and want to make sure you see this. Brian had requested that we move this component to frontend-js since it is being referenced and used in the objects module, here. If you think we need to change anything here, let me know

@liferay-continuous-integration
Copy link
Collaborator

❌ ci:test:stable - 10 out of 11 jobs passed

❌ ci:test:relevant - 21 out of 25 jobs passed in 2 hours

Click here for more details.

This pull is eligible for reevaluation. When this upstream build has completed, using the following CI command will compare this pull request result against a more recent upstream result:

ci:reevaluate:1297401_283

Base Branch:

Branch Name: master
Branch GIT ID: 77d84cd9f7f6c106d9a42bdee06a57003211ee91

Upstream Comparison:

Branch GIT ID: 601787d738f00717211a50876d0873ebabcd8035
Jenkins Build URL: Acceptance Upstream DXP (master) #2320

ci:test:stable - 10 out of 11 jobs PASSED
10 Successful Jobs:
ci:test:relevant - 21 out of 25 jobs PASSED
21 Successful Jobs:
For more details click here.

Failures unique to this pull:

  1. test-portal-acceptance-pullrequest-batch(master)/js-unit-jdk8/0
    Job Results:

    0 Tests Passed.
    1 Test Failed.

    1. AXIS_VARIABLE=0 #469608
           [exec] * What went wrong:
           [exec] Execution failed for task ':apps:commerce:commerce-frontend-js:packageRunTest'.
           [exec] > Process 'command '/opt/dev/projects/github/liferay-portal/build/node/bin/node'' finished with non-zero exit value 1
           [exec] 
           [exec] * Try:
           [exec] Run with --info or --debug option to get more log output. Run with --scan to get full insights.
           [exec] 
           [exec] * Exception is:
           [exec] org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':apps:commerce:commerce-frontend-js:packageRunTest'.
           [exec] 	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.lambda$executeIfValid$1(ExecuteActionsTaskExecuter.java:208)
           [exec] 	at org.gradle.internal.Try$Failure.ifSuccessfulOrElse(Try.java:263)
           [exec] 	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeIfValid(ExecuteActionsTaskExecuter.java:206)
           [exec] 	at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:187)
           [exec] 	at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:114)
           [exec] 	at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:46)
           [exec] 	at org.gradle.api.internal.tasks.execution.ResolveTaskExecutionModeExecuter.execute(ResolveTaskExecutionModeExecuter.java:62)
           [exec] 	at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:57)
           [exec] 	at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:56)
           [exec] 	at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
           [exec] 	at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:77)
           [exec] 	at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:55)
           [exec] 	at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
           [exec] 	at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:409)
  2. test-portal-acceptance-pullrequest-batch(master)/modules-integration-mysql57-jdk8/0
    Job Results:

    3237 Tests Passed.
    4 Tests Failed.

    1. AXIS_VARIABLE=5 #234883
      1. com.liferay.object.admin.rest.resource.v1_0.test.ObjectDefinitionResourceTest.testGetObjectDefinitionsPage
        java.lang.AssertionError: expected:<2> but was:<3>
        	at org.junit.Assert.fail(Assert.java:89)
        	at org.junit.Assert.failNotEquals(Assert.java:835)
        	at org.junit.Assert.assertEquals(Assert.java:647)
        	at org.junit.Assert.assertEquals(Assert.java:633)
        	at com.liferay.object.admin.rest.resource.v1_0.test.BaseObjectDefinitionResourceTestCase.testGetObjectDefinitionsPage(BaseObjectDefinitionResourceTestCase.java:222)
        	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        	at java.lang.reflect.Method.invoke(Method.java:498)
        	at com.liferay.arquillian.extension.junit.bridge.server.TestExecutorRunnable$3.evaluate(TestExecutorRunnable.java:353)
        	at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
        	at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.Abs...
      2. com.liferay.object.admin.rest.resource.v1_0.test.ObjectLayoutResourceTest.testGetObjectDefinitionObjectLayoutsPage
        java.lang.AssertionError: Thread Thread[http-nio-8080-exec-12,5,main] caught concurrent failure: java.lang.AssertionError: {level=ERROR, loggerName=com.liferay.portal.vulcan.internal.jaxrs.exception.mapper.ExceptionMapper, message=com.liferay.object.exception.DefaultObjectLayoutException
        java.lang.AssertionError: {level=ERROR, loggerName=com.liferay.portal.vulcan.internal.jaxrs.exception.mapper.ExceptionMapper, message=com.liferay.object.exception.DefaultObjectLayoutException
        	at com.liferay.portal.test.rule.LogAssertionTestRule$LogAppender.append(LogAssertionTestRule.java:330)
        	at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)
        	at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)
        	at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)
        	at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)
        	at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:540)
        	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:498)
        	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:481)
        	at org.apache.logging.log4j.core.config.LoggerConfig.logParent(LoggerConfig.java:531)
        	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:500)
        	at org.apache.logging.log4j.core.config.LoggerConfig.log(Logger...
      3. com.liferay.object.admin.rest.resource.v1_0.test.ObjectLayoutResourceTest.testGetObjectDefinitionObjectLayoutsPageWithPagination
        java.lang.AssertionError: Thread Thread[http-nio-8080-exec-9,5,main] caught concurrent failure: java.lang.AssertionError: {level=ERROR, loggerName=com.liferay.portal.vulcan.internal.jaxrs.exception.mapper.ExceptionMapper, message=com.liferay.object.exception.DefaultObjectLayoutException
        java.lang.AssertionError: {level=ERROR, loggerName=com.liferay.portal.vulcan.internal.jaxrs.exception.mapper.ExceptionMapper, message=com.liferay.object.exception.DefaultObjectLayoutException
        	at com.liferay.portal.test.rule.LogAssertionTestRule$LogAppender.append(LogAssertionTestRule.java:330)
        	at org.apache.logging.log4j.core.config.AppenderControl.tryCallAppender(AppenderControl.java:156)
        	at org.apache.logging.log4j.core.config.AppenderControl.callAppender0(AppenderControl.java:129)
        	at org.apache.logging.log4j.core.config.AppenderControl.callAppenderPreventRecursion(AppenderControl.java:120)
        	at org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:84)
        	at org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:540)
        	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:498)
        	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:481)
        	at org.apache.logging.log4j.core.config.LoggerConfig.logParent(LoggerConfig.java:531)
        	at org.apache.logging.log4j.core.config.LoggerConfig.processLogEvent(LoggerConfig.java:500)
        	at org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerC...
    2. AXIS_VARIABLE=6 #234883
      1. com.liferay.object.service.test.ObjectDefinitionLocalServiceTest.testAddSystemObjectDefinition
        java.lang.AssertionError
        	at org.junit.Assert.fail(Assert.java:87)
        	at org.junit.Assert.fail(Assert.java:96)
        	at com.liferay.object.service.test.ObjectDefinitionLocalServiceTest.testAddSystemObjectDefinition(ObjectDefinitionLocalServiceTest.java:822)
        	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        	at java.lang.reflect.Method.invoke(Method.java:498)
        	at com.liferay.arquillian.extension.junit.bridge.server.TestExecutorRunnable$3.evaluate(TestExecutorRunnable.java:353)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$2.evaluate(AbstractTestRule.java:99)
        	at org.junit.rules.RunRules.evaluate(RunRules.java:20)
        	at com.liferay.arquillian.extension.junit.bridge.server.TestExecutorRunnable$1.evaluate(TestExecutorRunnable.java:237)
        	at com.liferay.portal.kernel.test.rule.AbstractTestRule$1.evaluate(AbstractTestRule.java:59)
        	at...

Failures in common with acceptance upstream results at 601787d:
  1. test-portal-acceptance-pullrequest-batch(master)/modules-integration-mysql57-jdk8/0
    Job Results:

    3237 Tests Passed.
    4 Tests Failed.

    1. AXIS_VARIABLE=6 #234883
      1. com.liferay.portal.log.assertor.PortalLogAssertorTest.testScanXMLLog
        junit.framework.AssertionFailedError: 
        Unable to connect to a valid mail server. Please make sure one is properly configured: Couldn't connect to host, port: localhost, 25; timeout -1
        	at com.liferay.portal.log.assertor.PortalLogAssertorTest.scanXMLLogFile(PortalLogAssertorTest.java:171)
        	at com.liferay.portal.log.assertor.PortalLogAssertorTest$1.visitFile(PortalLogAssertorTest.java:98)
        	at com.liferay.portal.log.assertor.PortalLogAssertorTest$1.visitFile(PortalLogAssertorTest.java:88)
        	at java.nio.file.Files.walkFileTree(Files.java:2670)
        	at java.nio.file.Files.walkFileTree(Files.java:2742)
        	at com.liferay.portal.log.assertor.PortalLogAssertorTest.testScanXMLLog(PortalLogAssertorTest.java:86)
        
Test bundle downloads:

@liferay-continuous-integration
Copy link
Collaborator

url.searchParams.append('pageSize', pageSize);
}

return Liferay.Util.fetch(url, {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can import it from frontend-js-web together with {debounce}. It would be great if, together with [no-global-fetch](https://github.com/liferay/liferay-frontend-projects/blob/master/projects/eslint-config/plugins/portal/lib/rules/no-global-fetch.js) we could make sure that we use the imported one when in a module context


return Liferay.Util.fetch(url, {
headers: new Headers({
Accept: 'application/json',
Copy link

@jbalsas jbalsas Sep 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've seen this a couple of times around already. Wondering if we'd want to:

  • Create some type of fetchJSON utility on top of our fetch (probably not, but it's an option)
  • Provide something like FETCH_HEADERS_JSON


getData(apiURL, query, 1, 10)
.then((jsonResponse) => {
updateItems(jsonResponse.items);
Copy link

@jbalsas jbalsas Sep 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't this be unsafe from an isMounted point of view? By the time this runs, something else could've happened. I think we have useStateSafe for this, which will be rendered useless by facebook/react#22114 anyways

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah we probably should use useStateSafe for this and I will update it.

From a memory perspective, this isn't a terrible issue because it likely would only happen once and would not be a re-occuring memory leak like an event listener would be. Its sort of "memory splash"™️ 😂

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually the more I think about it, and going off what Dan says

This will trigger the setState warning. However, there is no actual problem here. There's no "memory leak" here: the POST comes back, we try to set state, the component is unmounted (so nothing happens), and then we're done. Nothing keeps holding onto the component indefinitely.

I think we are fine to ignore this case for now and not go overboard with the useSafeState for cases that don't "hold onto the component indefinitely"

const [query, setQuery] = useState(initialLabel || '');

const [selectedItem, updateSelectedItem] = useState(initialValue || value);
const [items, updateItems] = useState(null);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this where Require that React.useState returned arguments has consistent naming pattern comes from? 😁

Are you planning on fixing this later in the process of the PR, or wait for the rule to be enforced?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbalsas I was waiting to enforce is until the rule is in, and thats sort of the pattern I am following for things right now. If I can create an eslint rule for it, I don't bother leaving that in my review yet because I that way we can just make that change all at once.

For this code specifically, I just copied it from the old implementation and didn't get the standards too much thought at the time, but as I go through it now I am updating it and creating any issues for rules we need for eslint.

Bundle-Version: 1.0.0
Web-ContextPath: /object-dynamic-data-mapping
Copy link

@jbalsas jbalsas Sep 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-web modules don't usually have a Web-ContextPath (with some exceptions like -taglib). Apparently -form-field-type are also allowed. Not sure if Brian will go for this, but I think it might be safer to follow one of these and split the package:

@@ -47,7 +47,7 @@

@Override
public String getModuleName() {
return "commerce-frontend-js/components/autocomplete/Autocomplete";
return "object-dynamic-data-mapping/Autocomplete";
Copy link

@jbalsas jbalsas Sep 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how this could really work. If I'm not mistaken, this relies on object-dynamic-data-mapping/Autocomplete to be aliased and exposed as a global module. This needs to happen via .npmbridgerc, which commerce-frontend-js does which is not really recommended.

As long as you're using a module from the same package, you should be able to use npmResolver to get the right path. See:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbalsas is this even the right way to use a js module at all? Brian wanted this autocomplete component moved out of commerce since it was now referenced in objects, but I wasn't even sure this was the right way for objects to consume a js component.

Copy link

@jbalsas jbalsas Sep 17, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbalsas is this even the right way to use a js module at all?

Yeah, I think this is legit.

This eventually gets through some layers in ddm until it gets to a Liferay.Loader.require call at data-engine's Field component

In our revisited case, the module is accessing an internal component, which doesn't break our principle of just accessing other modules by their main exports.

In the previous code, objects was accessing commerce's module through a path which should be internal to them. This only worked because of the bridge mechanism that exposes the whole commerce-frontend-js tree. So yeah, accessing external modules by path (digging in their internals) should be persecuted 👊

I'm not sure if I made a good job at answering, so please, let me know if this is still unclear 😂

labelKey,
onItemClick,
valueKey,
}) => {
Copy link

@jbalsas jbalsas Sep 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we provide some defaults here, at least for maybe labelKey and valueKey?

I see some disconnect between this and how it is eventually used in this case

<BaseAutocomplete
	labelKey={itemsLabel}
	valueKey={itemsKey}
/>

Which in turn goes up the chain. I think it might be too easy to forget to pass things that are required for the default behaviour to work properly, making the default a bit less useful.

A slight improvement would be to have this APIs typed. Then, at the very least, one could know what's needed ahead of time. #justsayin ;)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to change as little as possible on the commerce side which is why there is a disconnect. But now I am just thinking I should re-write the commerce component to be a bit more in-line with our existing patterns.

required,
}) {
const [query, setQuery] = useState(initialLabel || '');
const [initialised, setInitialised] = useState(Boolean(CustomView));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason this caught my eye... I didn't know we were switching to british english 😂

queen

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sometimes its important to pay respects

import React, {useState} from 'react';

function getData(apiUrl, query, page, pageSize) {
const url = new URL(apiUrl, Liferay.ThemeDisplay.getPortalURL());
Copy link

@jbalsas jbalsas Sep 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's also just themeDisplay. I don't know which is both, but maybe we should settle for one type of access to the object or another and enforce it as much as we can.

I took a quick look some time ago to see if we could place this elsewhere other than a big global but couldn't come up with anything of substance.


if (pageSize) {
url.searchParams.append('pageSize', pageSize);
}
Copy link

@jbalsas jbalsas Sep 16, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a comment here about the use of append vs set. Doing a quick grep in our codebase I see:

  • searchParams.set: 40 results in 28 files
  • searchParams.append: 62 results in 24 files

I bring this up because append will always append a new key-value pair regardless of it existing it previously in the URL.

So, if the url happened to be: foo.com?page=1, then append('page', 2) would generate foo.com?page=1&page=2. It would be up to the server then to handle page as a list param, but more often than not, first one will win and the effect of setting the param page to 2 would be negated.

I think in this case it's fairly safe because we are more or less certain themeDisplay.getPortalURL doesn't have search params and we're not passing this url around where others could manipulate it.

I'm not sure if we'd want to do something about this or even how we'd do it, but I get the feeling we're not always being 100% intentional when using these APIs, what could result in hard-to-diagnose issues in the future.

Comment on lines 158 to 160
setPage={setPage}
setPageSize={setInternalPageSize}
setSelectedItem={setSelectedItem}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you're just moving this component to another module/folder but it think it can be changed to a callback or a reducer(depending the depth)

We can see many traverse of state setters everywhere in this code :/

@bryceosterhaus
Copy link
Collaborator Author

@jbalsas @diegonvs I updated with most suggestions. Commit history got messed up form a merge conflict so I just force pushed changes. Not sure what it'll do to your comments...

@liferay-continuous-integration
Copy link
Collaborator

✔️ ci:test:sf - 1 out of 1 jobs passed in 3 minutes

Click here for more details.

Base Branch:

Branch Name: master
Branch GIT ID: 6c0fd4fd2e8baf6223d9d426eb80f1e69058d46e

Sender Branch:

Branch Name: LPS-139246
Branch GIT ID: 664451354fc0e9ca40eafb0983bcfbef5fcf470f

1 out of 1jobs PASSED
1 Successful Jobs:
For more details click here.

@liferay-continuous-integration
Copy link
Collaborator

Comment on lines 75 to 94
useEffect(() => {
function handleClick(event) {
if (
autocompleteRef.current.contains(event.target) ||
(dropdownRef.current &&
dropdownRef.current.contains(event.target))
) {
return;
}

setActive(false);
}
if (active) {
document.addEventListener('mousedown', handleClick);
}

return () => {
document.removeEventListener('mousedown', handleClick);
};
}, [active]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can hopefully be removed later when we expose onClickOutside from clay

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's support this, I'm going to create an issue.

Comment on lines 37 to 41
headers: new Headers({
Accept: 'application/json',
'Accept-Language': themeDisplay.getBCP47LanguageId(),
'Content-Type': 'application/json',
}),
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jbalsas I am going to leave this as is for now. I looked into exposing it as a constant with fetch but there are a handful of use cases that need to be thought through. I created a task for this. https://issues.liferay.com/browse/LPS-139556

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accept and Content-Type are not needed AFAIK if we want to rely on defaults.

const CustomView = props.customView || FetchedCustomView;

useEffect(() => {
if (items && items.length === 1 && props.autofill) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autofill was never actually used so I removed the prop and this effect

const value =
selectedItem && getValueFromItem(selectedItem, props.itemsKey);

if (props.id) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

id is also never used so I was able to remove this entire effect

Comment on lines 83 to 89
if (onValueUpdated) {
onValueUpdated(value, selectedItem);
}

if (onChange) {
onChange({target: {value}});
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These props are never used. Meaning <Autocomplete is never used and provided these props.

I am starting to think this component may have just been originally copied from some other place and a bunch of this logic got included. Or perhaps it used to be necessary and was never removed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bryceosterhaus onChange({target: {value}}); is needed to work with DDMForm was just added last week, it required in all DDM forms Fields

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also onValueUpdated, was created for a purpose (https://issues.liferay.com/browse/COMMERCE-4265) and it's being used here:

6 results - 5 files

modules/apps/commerce/commerce-catalog-web/src/main/resources/META-INF/resources/commerce_catalog/details.jsp

modules/apps/commerce/commerce-channel-web/src/main/resources/META-INF/resources/commerce_channel/general.jsp

modules/apps/commerce/commerce-pricing-web/src/main/resources/META-INF/resources/commerce_price_lists/commerce_price_list/details.jsp

modules/apps/commerce/commerce-product-definitions-web/src/main/resources/META-INF/resources/definition/add_cp_definition.jsp

modules/apps/commerce/commerce-product-definitions-web/src/main/resources/META-INF/resources/definition/duplicate_cp_definition.jsp

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FabioDiegoMastrorilli thanks for pointing those instances out, I hadn't seen them before. I will check it out.

updatePage(1);
updateTotalCount(null);
updateLastPage(null);
}, [props.infiniteScrollMode, query]);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

infiniteScrollMode is never toggled and I moved some of this logic to where query is changed. It is better to update state in a central location rather than updating it once and then updating it again on state change.

1 render is better than 2 renders

Comment on lines 100 to 102
setQuery(val);
setSelectedItem(null);
fetchData();

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty weird, I think fetchData is working exactly because of debounce, because we set the query here and it's asynchronous, so we call fetchData right away.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh good point, I thought something seemed off when I was writing this but I just ignored the feeling 😂

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha 😂

Comment on lines 62 to 88
const fetchData = debounce(() => {
if (isMounted()) {
setLoading(true);

getData(apiURL, query, 1, 10)
.then((jsonResponse) => {
setItems(jsonResponse.items);

setLoading(false);

if (!query) {
return;
}

const found = jsonResponse.items.find(
(item) => item[labelKey] === query
);

if (found) {
setSelectedItem(found);
}
})
.catch(() => {
setLoading(false);
});
}
}, 500);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this could be replaced with useResource.

import {fetch} from 'frontend-js-web';

const [networkStatus, setNetworkStatus] = useState(4);
const {resource: items} = useResource({
	fetch,
	fetchPolicy: 'cache-first',
	link: new URL(apiUrl, themeDisplay.getPortalURL()),
	onNetworkStatusChange: setNetworkStatus,
	variables: {
		search: query,
		page: 1,
		pageSize: 10
	}
});

const initialLoading = networkStatus === 1;
const loading = networkStatus < 4;
const error = networkStatus === 5;

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try this out!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up using this in one place but not the other. I didn't use it for the infinite scroll because I thought it would be odd to refetch and update the list in a useEffect.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm I think I saw this case in the ListView, maybe we can use, as I understand the rule, it calls fetchData if the value of query changes, in this case with useResource and the query remains available in variables the useResource will automatically request again and thus no longer need useEffect for this. In the onBottomTouched call it's also no longer necessary because you're going to be using page as part of variables too:

const {resource: items} = useResource({
	...
	variables: {
		search: query,
		page,
		pageSize,
	}
});

Does it make sense? I can be missing something.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My concern is that we have to still maintain a list of all items requested since the list grows on scroll. So if we use useResource, we have to use an effect for updating the list. Unless there is another way to join the new requested items with the old requested items because we want to render page 1 + page 2 and not just render one or the other.

const [listItems, setListItems] = useState([]);

const {resource: items} = useResource({
	...
	variables: {
		search: query,
		page,
		pageSize,
	}
});

useEffect(() => {
	setListItems([...listItems, ...items]);
}, [items])

If we rely on the useEffect, I think that would cause multiple re-renders, once when items is updated with page 2 and then again when we add that with setListItems

Copy link

@matuzalemsteles matuzalemsteles Sep 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, now this makes more sense to me, in fact using useResource wouldn't help in this case, I'm going through the same situation in implementing Tree View for loading more items and maybe useResource will support this via OOTB, still "drawing" as the API would be...

But I agree. Maybe it could just remove the fetchData implementation and shorten the code but still having to use useEffect or useMemo can help too.

Comment on lines 75 to 94
useEffect(() => {
function handleClick(event) {
if (
autocompleteRef.current.contains(event.target) ||
(dropdownRef.current &&
dropdownRef.current.contains(event.target))
) {
return;
}

setActive(false);
}
if (active) {
document.addEventListener('mousedown', handleClick);
}

return () => {
document.removeEventListener('mousedown', handleClick);
};
}, [active]);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's support this, I'm going to create an issue.


return (
<>
<FocusScope>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not totally sure if this is even necessary, it was copied from the original commerce code

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah no problem, I figured.

);
};

function Autocomplete({

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @bryceosterhaus looking at this well maybe we can support this component in Clay as a high-level component, it looks like you've eliminated a lot of commerce dependencies here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I was thinking we could investigate this as a next step. My first step is just getting the commerce dependency out of DDM. Once this is merged, I can open a separate issue to investigate either moving this to Clay or taking advantage of our existing autocomplete component

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I agree, it doesn't need to be done now, we can check it out later and we can take care of that too.

@bryceosterhaus
Copy link
Collaborator Author

Just updated with corrections for how the UI behaves in commerce. We ended up not needing the autocomplete component at all.
Forms
Screen Shot 2021-09-21 at 4 57 48 PM

Commerce
Screen Shot 2021-09-21 at 4 59 03 PM
Screen Shot 2021-09-21 at 4 59 09 PM

@marco-leo @FabioDiegoMastrorilli can your team test this out to make sure I didn't miss anything? From my tests I think this should be working as it did before this refactor

@bryceosterhaus
Copy link
Collaborator Author

Closing in favor of #1489

@FabioDiegoMastrorilli, no rush right now, but I would encourage your team to look into getting rid of those createPortals(ReactPortal) you are using to render the content elsewhere. That sort of pattern isn't super great and will become difficult to maintain over time. I would always recommend opting for a uni-directional data flow. You can check out my work in this commit to see how I got rid of that sort of pattern or feel free to bother me if you need any help in the future.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants