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

refactor: Migrate preselected filters component #1422

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
abd1332
feat: WIP refactor component
annacv Feb 20, 2024
8e7bb45
test: WIP type mounted Component
annacv Feb 20, 2024
928f86e
Merge branch 'main' of https://github.com/empathyco/x into feature/EM…
annacv Feb 20, 2024
ff2d75b
feat: useNoElementRender composable in component
annacv Feb 20, 2024
cdc2dd8
feat: update branch + component with cpomputed & watcher
annacv Feb 21, 2024
91f5246
chore: rm unused
annacv Feb 21, 2024
1facf0f
Update packages/x-components/src/x-modules/facets/components/preselec…
annacv Feb 26, 2024
c882abe
Update packages/x-components/src/x-modules/facets/components/preselec…
annacv Feb 26, 2024
b905727
Update packages/x-components/src/x-modules/facets/components/preselec…
annacv Feb 26, 2024
0cd0688
feat: use XBus Composable
annacv Feb 26, 2024
c5c7997
feat: rollback useXBus as it does not inject the current location
annacv Feb 26, 2024
c239e8d
feat: make x-bus-composable return 'none' location by default
annacv Feb 29, 2024
274277c
feat: use x-bus-composable in preselected filters
annacv Feb 29, 2024
896ddf3
chore: WIP
annacv Mar 1, 2024
619b397
feat: set the injected snippet as a ref to get it sync when it changes
annacv Mar 4, 2024
a5ea863
test: spy bus emit f() & add emit event metadata
annacv Mar 4, 2024
142b086
Merge branch 'main' of https://github.com/empathyco/x into feature/EM…
annacv Mar 4, 2024
078f700
feat: use hybrid inject
annacv Mar 4, 2024
d268201
chore: rm template & use no elem renderer
annacv Mar 4, 2024
1532f21
Update packages/x-components/src/x-modules/facets/components/preselec…
annacv Mar 5, 2024
42ec203
Update packages/x-components/src/x-modules/facets/components/__tests_…
annacv Mar 5, 2024
2e54d17
Update packages/x-components/src/x-modules/facets/components/preselec…
annacv Mar 5, 2024
0c98c95
Update packages/x-components/src/x-modules/facets/components/preselec…
annacv Mar 5, 2024
b1c518f
chore: rm unused imports
annacv Mar 5, 2024
7ee475b
tests: replace eventMetadata object by any object
annacv Mar 5, 2024
58b9b84
feat: use render() option instead of returning useNoElementRendered()
annacv Mar 6, 2024
1b33509
don't destructure composable and add internal fields to doc
CachedaCodes Mar 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/x-components/src/composables/use-x-bus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { PropsWithType } from '../utils/index';
* @returns An object with the `on` and `emit` functions.
*/
export function useXBus(): UseXBusAPI {
const location = inject<FeatureLocation>('location');
const location = inject<FeatureLocation>('location', 'none');

const currentComponent: PrivateExtendedVueComponent | undefined | null =
getCurrentInstance()?.proxy;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { createLocalVue, mount, Wrapper } from '@vue/test-utils';
import Vue from 'vue';
import Vue, { ComponentOptions } from 'vue';
import Vuex from 'vuex';
import { Dictionary } from '@empathyco/x-utils';
import { createRawFilters } from '../../../../utils/filters';
import { baseSnippetConfig } from '../../../../views/base-config';
import PreselectedFilters from '../preselected-filters.vue';
import { bus } from '../../../../plugins/index';

function renderPreselectedFilters({
filters,
snippetFilters
}: RenderPreselectedFiltersOptions = {}): RenderPreselectedFiltersAPI {
const emit = jest.fn();
const emit = jest.spyOn(bus, 'emit');
const localVue = createLocalVue();
const snippetConfig = Vue.observable({ ...baseSnippetConfig, filters: snippetFilters });
localVue.use(Vuex);

const wrapper = mount(PreselectedFilters, {
const wrapper = mount(PreselectedFilters as ComponentOptions<Vue>, {
provide: {
snippetConfig: snippetConfig
},
Expand All @@ -24,12 +25,18 @@ function renderPreselectedFilters({
},
localVue,
mocks: {
$x: {
emit: {
emit
}
}
});

const eventMetadata = {
moduleName: null,
location: 'none',
replaceable: true
};

Copy link
Contributor

Choose a reason for hiding this comment

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

If you don't care what the metadata is in the tests, you can use expect.any(Object).
Remove this and use that instead.

function setSnippetConfig(newValue: Dictionary<unknown>): Promise<void> {
Object.assign(snippetConfig, newValue);
return localVue.nextTick();
Expand All @@ -38,7 +45,8 @@ function renderPreselectedFilters({
return {
wrapper,
emit,
setSnippetConfig
setSnippetConfig,
eventMetadata
};
}

Expand All @@ -49,7 +57,6 @@ describe('testing Preselected filters component', () => {

it('does not emit the event when neither filters nor snippet config filters are provided', () => {
const { emit } = renderPreselectedFilters();

expect(emit).not.toHaveBeenCalled();
});

Expand All @@ -58,25 +65,30 @@ describe('testing Preselected filters component', () => {
'{!tag=brand_facet}brand_facet:"Lego"',
'{!tag=age_facet}age_facet:"toddler"'
];
const { emit } = renderPreselectedFilters({
const { emit, eventMetadata } = renderPreselectedFilters({
snippetFilters
});

expect(emit).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith(
'PreselectedFiltersProvided',
createRawFilters(snippetFilters)
createRawFilters(snippetFilters),
eventMetadata
annacv marked this conversation as resolved.
Show resolved Hide resolved
);
});

it('emits the event when filters are provided by the prop', () => {
const filters = ['{!tag=brand_facet}brand_facet:"Lego"', '{!tag=age_facet}age_facet:"toddler"'];
const { emit } = renderPreselectedFilters({
const { emit, eventMetadata } = renderPreselectedFilters({
filters
});

expect(emit).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith('PreselectedFiltersProvided', createRawFilters(filters));
expect(emit).toHaveBeenCalledWith(
'PreselectedFiltersProvided',
createRawFilters(filters),
eventMetadata
);
});

it('emits the event using the snippet config filters as payload when both are provided', () => {
Expand All @@ -85,48 +97,61 @@ describe('testing Preselected filters component', () => {
'{!tag=brand_facet}brand_facet:"Nintendo"',
'{!tag=age_facet}age_facet:"kids"'
];
const { emit } = renderPreselectedFilters({
const { emit, eventMetadata } = renderPreselectedFilters({
filters,
snippetFilters
});

expect(emit).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith(
'PreselectedFiltersProvided',
createRawFilters(snippetFilters)
createRawFilters(snippetFilters),
eventMetadata
);
});

it('emits the event when the prop filters change', async () => {
const filters = ['{!tag=brand_facet}brand_facet:"Lego"'];
const newFilters = ['{!tag=brand_facet}brand_facet:"Playmobil"'];

const { emit, wrapper } = renderPreselectedFilters({
const { emit, eventMetadata, wrapper } = renderPreselectedFilters({
filters
});

expect(wrapper.props()).toEqual({ filters: filters });
expect(emit).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith('PreselectedFiltersProvided', createRawFilters(filters));
expect(emit).toHaveBeenCalledWith(
'PreselectedFiltersProvided',
createRawFilters(filters),
eventMetadata
);

await wrapper.setProps({ filters: newFilters });

expect(wrapper.props()).toEqual({ filters: newFilters });
expect(emit).toHaveBeenCalledTimes(2);
expect(emit).toHaveBeenCalledWith('PreselectedFiltersProvided', createRawFilters(newFilters));
expect(emit).toHaveBeenCalledWith(
'PreselectedFiltersProvided',
createRawFilters(newFilters),
eventMetadata
);
});

it('emits the event when the snippetConfig filters change', async () => {
const filters = ['{!tag=brand_facet}brand_facet:"Chorizo"'];
const newFilters = ['{!tag=brand_facet}brand_facet:"Chistorra"'];

const { emit, wrapper, setSnippetConfig } = renderPreselectedFilters({
const { emit, eventMetadata, wrapper, setSnippetConfig } = renderPreselectedFilters({
filters
});

expect(wrapper.props()).toEqual({ filters: filters });
expect(emit).toHaveBeenCalledTimes(1);
expect(emit).toHaveBeenCalledWith('PreselectedFiltersProvided', createRawFilters(filters));
expect(emit).toHaveBeenCalledWith(
'PreselectedFiltersProvided',
createRawFilters(filters),
eventMetadata
);

await setSnippetConfig({ filters: newFilters });

Expand All @@ -138,7 +163,11 @@ describe('testing Preselected filters component', () => {

// The event is called again with the newFilters provided
expect(emit).toHaveBeenCalledTimes(2);
expect(emit).toHaveBeenCalledWith('PreselectedFiltersProvided', createRawFilters(newFilters));
expect(emit).toHaveBeenCalledWith(
'PreselectedFiltersProvided',
createRawFilters(newFilters),
eventMetadata
);
});
});

Expand All @@ -157,9 +186,11 @@ interface RenderPreselectedFiltersOptions {
*/
interface RenderPreselectedFiltersAPI {
/** Mock of the {@link XBus.emit} function. */
emit: jest.Mock;
emit: jest.SpyInstance;
/** The wrapper of the container element.*/
wrapper: Wrapper<Vue>;
/** Helper method to change the snippet config. */
setSnippetConfig: (newSnippetConfig: Dictionary<unknown>) => void | Promise<void>;
/** Metadata object returned by the {@link XBus.emit} function. */
eventMetadata: Dictionary<unknown>;
}
Original file line number Diff line number Diff line change
@@ -1,69 +1,83 @@
<script lang="ts">
import { Component, Inject, Prop, Watch } from 'vue-property-decorator';
import Vue from 'vue';
import { defineComponent, PropType, onMounted, watch, computed, ref, ComputedRef } from 'vue';
import { createRawFilters } from '../../../utils/filters';
import { isArrayEmpty } from '../../../utils/array';
import { SnippetConfig } from '../../../x-installer/api/api.types';
import { useXBus } from '../../../composables/use-x-bus';
import { useHybridInject, useNoElementRender } from '../../../composables';
import { baseSnippetConfig } from '../../../views/base-config';

/**
* This component emits {@link FacetsXEvents.PreselectedFiltersProvided} when a preselected filter
* is set in the snippet config or by using the prop of the component.
*
* @public
*/
@Component
export default class PreselectedFilters extends Vue {
/**
* Injects {@link SnippetConfig} provided by an ancestor as snippetConfig.
*
* @internal
*/
@Inject('snippetConfig')
public snippetConfig?: SnippetConfig;

/**
* A list of filters to preselect.
*
* @remarks Emits the {@link FacetsXEvents.PreselectedFiltersProvided} when the
* component is created.
*
* @public
*/
@Prop({ default: () => [] })
public filters!: string[];

/**
* Gets the provided preselected filters prioritizing the {@link SnippetConfig} over the
* filters prop.
*
* @returns An array of filter's ids.
*/
protected get preselectedFilters(): string[] {
return this.snippetConfig?.filters ?? this.filters;
}

/**
* Emits the {@link FacetsXEvents.PreselectedFiltersProvided} to save
* the provided filters in the state.
*/
@Watch('preselectedFilters')
protected emitPreselectedFilters(): void {
if (!isArrayEmpty(this.preselectedFilters)) {
this.$x.emit('PreselectedFiltersProvided', createRawFilters(this.preselectedFilters));
export default defineComponent({
name: 'PreselectedFilters',
props: {
/**
* A list of filters to preselect.
*
* @remarks Emits the {@link FacetsXEvents.PreselectedFiltersProvided} when the
* component is created.
*
* @public
*/
filters: {
type: Array as PropType<string[]>,
default: () => []
}
},
setup(props, { slots }) {
// eslint-disable-next-line @typescript-eslint/unbound-method
const { emit } = useXBus();

/**
* Injects {@link SnippetConfig} provided by an ancestor as snippetConfig
* and sets is as a ref to get synced when it changes.
*
* @internal
*/
const snippetConfig = ref(useHybridInject<SnippetConfig>('snippetConfig', baseSnippetConfig));
annacv marked this conversation as resolved.
Show resolved Hide resolved

/**
* Gets the provided preselected filters prioritizing the {@link SnippetConfig} over the
* filters prop.
*
* @returns An array of filter's ids.
*/
const preselectedFilters: ComputedRef<string[]> = computed(() => {
annacv marked this conversation as resolved.
Show resolved Hide resolved
return snippetConfig.value?.filters ?? props.filters;
});

/**
* Emits the {@link FacetsXEvents.PreselectedFiltersProvided} to save
* the provided filters in the state.
*/
const emitPreselectedFilters = (): void => {
if (!isArrayEmpty(preselectedFilters.value)) {
emit('PreselectedFiltersProvided', createRawFilters(preselectedFilters.value));
}
};

/**
* Emits the {@link FacetsXEvents.PreselectedFiltersProvided} when the
* computed prop changes.
*/
watch(preselectedFilters, emitPreselectedFilters);

/**
* Emits the {@link FacetsXEvents.PreselectedFiltersProvided} when the
* component is mounted.
*/
onMounted(() => {
emitPreselectedFilters();
});
annacv marked this conversation as resolved.
Show resolved Hide resolved

return () => useNoElementRender(slots);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the render function instead, it's more declarative.

Suggested change
return () => useNoElementRender(slots);
}
},
render() {
return useNoElementRender(this.$slots)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hi, if I'm not wrong render is only available if we use the Options API, for the Composition API we should use h and return it directly, which I think is almost the same we are doing now.

Copy link
Contributor

Choose a reason for hiding this comment

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

The defineComponent function receives an options object that is more or less the same type of object you create in the options API (you are using it already, passing the props and setting up a name). render is one of the options that you can send to defineComponent and will work with setup when setup doesn't use return


/**
* Emits the {@link FacetsXEvents.PreselectedFiltersProvided} when the
* component is created.
*/
created(): void {
this.emitPreselectedFilters();
}

// eslint-disable-next-line @typescript-eslint/no-empty-function
render(): void {}
}
});
</script>

<docs lang="mdx">
Expand Down
Loading