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

feat(headless): TriggerRedirection #875

Merged
merged 43 commits into from
Jun 21, 2021
Merged

feat(headless): TriggerRedirection #875

merged 43 commits into from
Jun 21, 2021

Conversation

nbuissoncoveo
Copy link
Contributor

https://coveord.atlassian.net/browse/KIT-548

Just a draft to make sure I'm on the right track

Comment on lines 19 to 22
const redirectTriggers: Trigger[] = action.payload.response.triggers.filter(
(trigger) => isTriggerRedirect(trigger)
);
state.redirectTo = (redirectTriggers[0] as TriggerRedirect).content;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wasn't sure how to handle the fact that response.triggers is an array of Trigger objects and that there might be multiple TriggerRedirect objects in the array.

Copy link
Contributor

Choose a reason for hiding this comment

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

It's good to filter, be careful thought, because not all responses contain triggers, even less redirect triggers

}

const optionsSchema = new Schema({
function: new Function(),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a compilation error on this line that I don't understand: Type 'Function' is missing the following properties from type 'SchemaValue<unknown>': validate, default, required

Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have anything in Bueno I think, it only needs to be defined, perhaps this would work, it would have to be tested

Suggested change
function: new Function(),
onRedirect: new Value({required: true}),

@nbuissoncoveo
Copy link
Contributor Author

Completed the controller's initial structure but I'm a bit confused about which actions the controller should have.

@nbuissoncoveo nbuissoncoveo requested a review from ThibodeauJF June 9, 2021 17:06
/**
* The function used to handle whenever the `Redirection` controller's state's `redirectTo` value changes.
*/
function: Function;
Copy link
Contributor

Choose a reason for hiding this comment

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

I would pick something more significant as a name, like onRedirect

Suggested change
function: Function;
onRedirect(): void;

}

const optionsSchema = new Schema({
function: new Function(),
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't have anything in Bueno I think, it only needs to be defined, perhaps this would work, it would have to be tested

Suggested change
function: new Function(),
onRedirect: new Value({required: true}),

}

/**
* A scoped and simplified part of the headless state that is relevant to the `Pager` controller.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* A scoped and simplified part of the headless state that is relevant to the `Pager` controller.
* A scoped and simplified part of the headless state that is relevant to the `RedirectionTrigger` controller.

/**
* The `Redirection` controller handles redirection actions.
*/
export interface Redirection extends Controller {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
export interface Redirection extends Controller {
export interface RedirectionTrigger extends Controller {

Copy link
Contributor

Choose a reason for hiding this comment

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

Also complete the name for other interfaces/methods


get state() {
return {
redirectTo,
Copy link
Contributor

Choose a reason for hiding this comment

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

you have to extract the state from the engine's state

validateOptions,
} from '../../utils/validate-payload';

export interface RedirectionInitialState {
Copy link
Contributor

Choose a reason for hiding this comment

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

there should be no initial state for that controller since it's triggered later depending on the response

props.initialState,
'buildRedirection'
);
const redirectTo = initialState.redirectTo;
Copy link
Contributor

Choose a reason for hiding this comment

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

We should not save local variables in controllers if possible and use the engine's state instead to save values

Comment on lines 19 to 22
const redirectTriggers: Trigger[] = action.payload.response.triggers.filter(
(trigger) => isTriggerRedirect(trigger)
);
state.redirectTo = (redirectTriggers[0] as TriggerRedirect).content;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's good to filter, be careful thought, because not all responses contain triggers, even less redirect triggers

@ThibodeauJF
Copy link
Contributor

Good for the scaffold, its still missing the most important part: to call the method (onRedirect) at the right moment, when a redirect trigger is returned by the API

@nbuissoncoveo nbuissoncoveo requested a review from ThibodeauJF June 9, 2021 20:43
Copy link
Contributor

@ThibodeauJF ThibodeauJF left a comment

Choose a reason for hiding this comment

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

I would consider using tests or Atomic or React code samples (packages/samples/headless-react) to make sure the feature works as expected while you implement it.

We will have to add RedirectionTrigger components to both Atomic & the react samples nevertheless


export interface RedirectionTriggerOptions {
/**
* The function used to handle whenever the `Redirection` controller's state's `redirectTo` value changes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
* The function used to handle whenever the `Redirection` controller's state's `redirectTo` value changes.
* The function used to handle whenever the `RedirectionTrigger` controller's state's `redirectTo` value changes.

also in other tsdocs

state.redirectTo = action.payload;
})
.addCase(executeSearch.fulfilled, (state, action) => {
const redirectTriggers: Trigger[] = action.payload.response.triggers.filter(
Copy link
Contributor

Choose a reason for hiding this comment

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

What if you don't set the type to Trigger, will it take on the TriggerRedirect type?

Copy link
Contributor Author

@nbuissoncoveo nbuissoncoveo Jun 10, 2021

Choose a reason for hiding this comment

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

I thought I had to specify the type, any does not seem to work. isTriggerRedirect only accepts params of the type Trigger so I don't see how I would be able to filter for only TriggerRedirect objects.

Copy link
Contributor

Choose a reason for hiding this comment

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

Should the triggers feature live in it's own slice?

It seems like a single state property now gets it's value from two different api calls (/plan and /search). I think we should separate the two concerns,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you thinking of doing one slice for all the triggers or having one slice per type of trigger?

Copy link
Contributor

@samisayegh samisayegh Jun 10, 2021

Choose a reason for hiding this comment

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

I was thinking one slice for all triggers, however it would be a good to spend some time documenting the design, comparing pros and cons of different approaches,

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should create a separate slice for triggers.

As is, if someone is using a standalone search box and a trigger, if the plan endpoint returns a url, then headless would log a redirection trigger event to UA which would be incorrect.

Copy link
Contributor

Choose a reason for hiding this comment

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

Goind point, agreed, the said slice's state should be split into the triggers types.

A con: would the redirection-slice become misleading then, if it's not affected by trigger on the /search call, but only on the /plan call ?

Comment on lines 22 to 24
state.redirectTo = redirectTriggers
? (redirectTriggers[0] as TriggerRedirect).content
: '';
Copy link
Contributor

Choose a reason for hiding this comment

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

You should check the length of the array, see if any is filtered, the array should always be defined, but it won't always contain elements. I don't think you should need the typecast, as per my previous comment

Suggested change
state.redirectTo = redirectTriggers
? (redirectTriggers[0] as TriggerRedirect).content
: '';
state.redirectTo = redirectTriggers.length
? redirectTriggers.content
: '';

/**
* The function used to handle whenever the `Redirection` controller's state's `redirectTo` value changes.
*/
onRedirect(): void;
Copy link
Contributor

Choose a reason for hiding this comment

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

The method could return the string of the redirectTo, but since it's already returned in the state, it's not really necessary, user will be able to access it.

Question of preference

'buildRedirectionTrigger'
) as Required<RedirectionTriggerOptions>;

const redirectTo = engine.state.redirection.redirectTo!;
Copy link
Contributor

Choose a reason for hiding this comment

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

Here, you only access the state value once, when the buildRedirection method is called, but it's not updated each time the engine state's changes. Here a few cues:

  • this line could be a method, returning the state, so every time it's called it's the current state
  • when you make the comparison, at line 78, it's with that original value, but it's never compared further, consider using the engine.subscribe method, you can also override the controller's own subscribe method, like it's done for headless-url-manager.ts

@github-actions
Copy link

github-actions bot commented Jun 10, 2021

Pull Request Report

PR Title

❌ Title should follow the conventional commit spec:

(optional scope):

Example:

feat(headless): add result-list controller

Bundle Size

File Compression Old (kb) New (kb) Change (%)
dist/browser/headless.js bundled 529.2 535.9 1.3
minified 218.3 220.7 1.1
gzipped 62.4 63 0.9
dist/browser/headless.esm.js bundled 512.7 519.1 1.2
minified 280 283.8 1.4
gzipped 68.4 69 0.9
dist/browser/case-assist/headless.js bundled 0.5 0.5 0
minified 0.3 0.3 0
gzipped 0.2 0.2 0
dist/browser/case-assist/headless.esm.js bundled 0.1 0.1 0
minified 0 0 0
gzipped 0.1 0.1 0
dist/browser/recommendation/headless.js bundled 351.8 352.2 0.1
minified 156.2 156.4 0.1
gzipped 45.3 45.3 0.1
dist/browser/recommendation/headless.esm.js bundled 342.7 343.1 0.1
minified 193.1 193.4 0.1
gzipped 49.5 49.5 0.1
dist/browser/product-recommendation/headless.js bundled 347.1 347.5 0.1
minified 154 154.2 0.1
gzipped 44.7 44.8 0.1
dist/browser/product-recommendation/headless.esm.js bundled 338 338.4 0.1
minified 191.1 191.3 0.1
gzipped 49 49 0.1

@@ -0,0 +1,106 @@
import {Schema, Value} from '@coveo/bueno';
Copy link
Contributor

Choose a reason for hiding this comment

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

Curious if there is a design doc for the trigger feature? Is the redirection trigger the only one in scope or is there a plan to support e.g. notify too?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am currently doing redirection but will work on notify and query once this is done.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, have the apis for each type been designed? Are they similar, or completely different? Depending on the answers, this will affect how things would best be named and structured.

If there is no design, I strongly recommend spending some time creating a document and sharing it with the team. It will us to collaborate and iterate faster compared to actually coding the feature,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The triggers.ts file in headless defines the interfaces for each trigger.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok, but will the controllers look like? Does it make sense to have a separate controller by type? Or would it be better to have one controller for everything related to triggers?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I remember meeting with Olivier and Jean-Felix and deciding on having a controller for each trigger type but I don't remember the reason for this choice.

Copy link
Contributor

Choose a reason for hiding this comment

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

We discussed the design together with Olivier and Nicolas, it's roughly described in the Jira https://coveord.atlassian.net/browse/KIT-548

* @param props - The configurable `Redirection` properties.
* @returns A `Redirection` controller instance.
* */
export function buildRedirection(
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't the name be buildRedirectionTrigger? Depending on what is in-scope, locking in the word redirection could also be too restrictive, hmm

const controller = buildController(engine);
const {dispatch} = engine;

let previousRedirectTo = '';
Copy link
Contributor

Choose a reason for hiding this comment

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

should be typed string | null and be instantiated to null

const hasChanged = previousRedirectTo !== this.state.redirectTo;
console.log('prev value', previousRedirectTo);
console.log('current controller state', this.state.redirectTo);
previousRedirectTo = this.state.redirectTo!;
Copy link
Contributor

Choose a reason for hiding this comment

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

You won't need the ! to force the type

@nbuissoncoveo nbuissoncoveo marked this pull request as ready for review June 17, 2021 14:52
Copy link
Contributor

@samisayegh samisayegh left a comment

Choose a reason for hiding this comment

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

My main comment would be the creation of a dedicated slice rather than share the redirection slice across two components,

redirectionTrigger.subscribe(listener);

expect(listener).toHaveBeenCalledTimes(1);
expect(getLogTriggerRedirectAction()).toBeTruthy();
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be possible to avoid logging an analytics event immediately on subscribe if this line was initialized to the value in state instead of null.

Copy link
Contributor

@ThibodeauJF ThibodeauJF Jun 18, 2021

Choose a reason for hiding this comment

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

Not sure I get this, we should log and call the listener on subscribe if the redirection state did contain a trigger (not null, or equivalent would be not an empty string)

Use cases could be SSR or creating a trigger controller later, after the latest search

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm thinking it should log on state change so headless does not log the same event over and over if someone instantiates multiple controllers.

We've said controllers should be instantiated before the first search. Allowing late registration feels inconsistent. UX-wise too, a user would see the results of their search and then suddenly a redirection when the late controller registers? I don't find that very nice,

I think it's better for redirection not to work in this situation, a) encouraging devs to adjust their code and b) avoiding questions around why instantiating one controller late "works", whereas others need to be instantiated before the search.

/**
* The url used for the redirection.
*/
redirectTo: string | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be possible to remove null from the type, and always initialize as a string.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not a hard argument from my part but: Its initialized as null in the slice/state as well, and checking against undefined/null is easier than for empty strings

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I initialized it to null for the case where a redirection trigger is received and saved to the engine's state but a redirection-trigger controller has not been initialized yet. Upon its initialization, it would need to immediately deal with that trigger and log it. If we immediately initialize it to the value in state then we would lose that functionality.

state.redirectTo = action.payload;
})
.addCase(executeSearch.fulfilled, (state, action) => {
const redirectTriggers: Trigger[] = action.payload.response.triggers.filter(
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should create a separate slice for triggers.

As is, if someone is using a standalone search box and a trigger, if the plan endpoint returns a url, then headless would log a redirection trigger event to UA which would be incorrect.

this.setState(this.controller.state);
}

render() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's remove the render function, or just return null, since there's no visual in the sample.

this.unsubscribe();
}

componentDidUpdate() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is not really needed, since the redirection can happen inside the updateState function.
The setState call is not necessary because the render function is not visual.

I would rename updateState to redirect and add the window.location.href line there,

Comment on lines 16 to 25
if (!state) {
return null;
}

if (state.redirectTo) {
window.location.href = state.redirectTo;
return null;
}

return <div></div>;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think can replace these lines with a simple return null. No need for a state hook too,

const redirect = () => {
     window.location.href = controller.state.redirectTo;
}

useEffect(() => controller.subscribe(redirect), []);

return null;

@@ -32,6 +32,12 @@ To run the unit tests and watch, run:
npm run test:watch
```

To use @coveo/headless locally, you have to build the package by running:
Copy link
Contributor

Choose a reason for hiding this comment

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

Great!

/**
* The url used for the redirection.
*/
redirectTo: string | null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a hard argument from my part but: Its initialized as null in the slice/state as well, and checking against undefined/null is easier than for empty strings

redirectionTrigger.subscribe(listener);

expect(listener).toHaveBeenCalledTimes(1);
expect(getLogTriggerRedirectAction()).toBeTruthy();
Copy link
Contributor

@ThibodeauJF ThibodeauJF Jun 18, 2021

Choose a reason for hiding this comment

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

Not sure I get this, we should log and call the listener on subscribe if the redirection state did contain a trigger (not null, or equivalent would be not an empty string)

Use cases could be SSR or creating a trigger controller later, after the latest search

state.redirectTo = action.payload;
})
.addCase(executeSearch.fulfilled, (state, action) => {
const redirectTriggers: Trigger[] = action.payload.response.triggers.filter(
Copy link
Contributor

Choose a reason for hiding this comment

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

Goind point, agreed, the said slice's state should be split into the triggers types.

A con: would the redirection-slice become misleading then, if it's not affected by trigger on the /search call, but only on the /plan call ?

@nbuissoncoveo nbuissoncoveo requested a review from samisayegh June 21, 2021 12:35
@ThibodeauJF ThibodeauJF requested a review from dlutzCoveo June 21, 2021 12:47
Copy link
Contributor

@samisayegh samisayegh left a comment

Choose a reason for hiding this comment

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

Left some final thoughts, but aside from that, looking good 💯

redirectionTrigger.subscribe(listener);

expect(listener).toHaveBeenCalledTimes(1);
expect(getLogTriggerRedirectAction()).toBeTruthy();
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm thinking it should log on state change so headless does not log the same event over and over if someone instantiates multiple controllers.

We've said controllers should be instantiated before the first search. Allowing late registration feels inconsistent. UX-wise too, a user would see the results of their search and then suddenly a redirection when the late controller registers? I don't find that very nice,

I think it's better for redirection not to work in this situation, a) encouraging devs to adjust their code and b) avoiding questions around why instantiating one controller late "works", whereas others need to be instantiated before the search.

@@ -57,6 +58,7 @@ export const searchHub = searchHubReducer;
export const debug = debugReducer;
export const resultPreview = resultPreviewReducer;
export const version = versionReducer;
export const trigger = triggerReducer;
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we use the plural triggers if the reducer is going to support all 4 triggers?

const redirect = () => {
setState(props.controller.state);
console.log(state);
console.log(props.controller.state);
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's remove these logs,

@nbuissoncoveo nbuissoncoveo merged commit 1439c9c into master Jun 21, 2021
@nbuissoncoveo nbuissoncoveo deleted the KIT-548 branch June 21, 2021 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants