-
Notifications
You must be signed in to change notification settings - Fork 36
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
Conversation
const redirectTriggers: Trigger[] = action.payload.response.triggers.filter( | ||
(trigger) => isTriggerRedirect(trigger) | ||
); | ||
state.redirectTo = (redirectTriggers[0] as TriggerRedirect).content; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
function: new Function(), | |
onRedirect: new Value({required: true}), |
Completed the controller's initial structure but I'm a bit confused about which actions the controller should have. |
/** | ||
* The function used to handle whenever the `Redirection` controller's state's `redirectTo` value changes. | ||
*/ | ||
function: Function; |
There was a problem hiding this comment.
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
function: Function; | |
onRedirect(): void; |
} | ||
|
||
const optionsSchema = new Schema({ | ||
function: new Function(), |
There was a problem hiding this comment.
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
function: new Function(), | |
onRedirect: new Value({required: true}), |
} | ||
|
||
/** | ||
* A scoped and simplified part of the headless state that is relevant to the `Pager` controller. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
export interface Redirection extends Controller { | |
export interface RedirectionTrigger extends Controller { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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
const redirectTriggers: Trigger[] = action.payload.response.triggers.filter( | ||
(trigger) => isTriggerRedirect(trigger) | ||
); | ||
state.redirectTo = (redirectTriggers[0] as TriggerRedirect).content; |
There was a problem hiding this comment.
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
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 |
There was a problem hiding this 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* 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( |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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,
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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,
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
state.redirectTo = redirectTriggers | ||
? (redirectTriggers[0] as TriggerRedirect).content | ||
: ''; |
There was a problem hiding this comment.
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
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; |
There was a problem hiding this comment.
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!; |
There was a problem hiding this comment.
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 forheadless-url-manager.ts
Pull Request Report PR Title ❌ Title should follow the conventional commit spec: (optional scope): Example: feat(headless): add result-list controller Bundle Size
|
@@ -0,0 +1,106 @@ | |||
import {Schema, Value} from '@coveo/bueno'; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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,
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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
packages/headless/src/controllers/triggers/headless-redirection-trigger.ts
Show resolved
Hide resolved
This reverts commit 2a578d5.
const controller = buildController(engine); | ||
const {dispatch} = engine; | ||
|
||
let previousRedirectTo = ''; |
There was a problem hiding this comment.
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!; |
There was a problem hiding this comment.
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
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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,
if (!state) { | ||
return null; | ||
} | ||
|
||
if (state.redirectTo) { | ||
window.location.href = state.redirectTo; | ||
return null; | ||
} | ||
|
||
return <div></div>; |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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 ?
There was a problem hiding this 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(); |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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,
https://coveord.atlassian.net/browse/KIT-548
Just a draft to make sure I'm on the right track