@mindful-web/marko-web-search 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.eslintignore +1 -0
  2. package/LICENSE +21 -0
  3. package/browser/.eslintrc.js +1 -0
  4. package/browser/index.js +9 -0
  5. package/browser/sort-select.vue +37 -0
  6. package/browser/toggle-filter-button.vue +69 -0
  7. package/browser/toggle-filters-button.vue +87 -0
  8. package/components/build-href.marko +14 -0
  9. package/components/build-href.marko.js +46 -0
  10. package/components/filters/content-types.marko +12 -0
  11. package/components/filters/content-types.marko.js +40 -0
  12. package/components/filters/filter-block.marko +66 -0
  13. package/components/filters/filter-block.marko.js +151 -0
  14. package/components/filters/filter-container.marko +29 -0
  15. package/components/filters/filter-container.marko.js +85 -0
  16. package/components/filters/load-website-sections.js +69 -0
  17. package/components/filters/marko.json +36 -0
  18. package/components/filters/selected/index.marko +51 -0
  19. package/components/filters/selected/index.marko.js +113 -0
  20. package/components/filters/selected/item.marko +27 -0
  21. package/components/filters/selected/item.marko.js +101 -0
  22. package/components/filters/selected/marko.json +8 -0
  23. package/components/filters/website-sections-by-alias.marko +46 -0
  24. package/components/filters/website-sections-by-alias.marko.js +139 -0
  25. package/components/filters/website-sections.marko +42 -0
  26. package/components/filters/website-sections.marko.js +123 -0
  27. package/components/form/index.marko +23 -0
  28. package/components/form/index.marko.js +79 -0
  29. package/components/form/input.marko +8 -0
  30. package/components/form/input.marko.js +30 -0
  31. package/components/form/marko.json +10 -0
  32. package/components/links/link.marko +14 -0
  33. package/components/links/link.marko.js +47 -0
  34. package/components/links/marko.json +17 -0
  35. package/components/links/next-page.marko +25 -0
  36. package/components/links/next-page.marko.js +59 -0
  37. package/components/links/previous-page.marko +25 -0
  38. package/components/links/previous-page.marko.js +59 -0
  39. package/components/links/reset-filter.marko +24 -0
  40. package/components/links/reset-filter.marko.js +55 -0
  41. package/components/links/set-filter-value.marko +45 -0
  42. package/components/links/set-filter-value.marko.js +86 -0
  43. package/components/marko.json +25 -0
  44. package/components/pagination-controls.marko +42 -0
  45. package/components/pagination-controls.marko.js +108 -0
  46. package/components/query-child-sections.marko +24 -0
  47. package/components/query-child-sections.marko.js +60 -0
  48. package/components/query.marko +30 -0
  49. package/components/query.marko.js +67 -0
  50. package/components/sort-by/index.marko +25 -0
  51. package/components/sort-by/index.marko.js +72 -0
  52. package/components/sort-by/marko.json +5 -0
  53. package/config/index.js +84 -0
  54. package/config/param.js +69 -0
  55. package/config/query-params.js +101 -0
  56. package/index.js +113 -0
  57. package/loaders/search.js +108 -0
  58. package/marko.json +6 -0
  59. package/middleware.js +9 -0
  60. package/package.json +32 -0
@@ -0,0 +1,69 @@
1
+ const { isFunction: isFn } = require('@mindful-web/utils');
2
+
3
+ class MarkoWebSearchQueryParam {
4
+ constructor({
5
+ name,
6
+ type,
7
+ defaultValue,
8
+ validator,
9
+ filter,
10
+ toInput,
11
+ fromInput,
12
+ } = {}) {
13
+ this.name = name;
14
+ this.type = type;
15
+ this.defaultValue = defaultValue;
16
+ this.validator = validator;
17
+ this.filter = filter;
18
+ this.toInput = toInput;
19
+ this.fromInput = fromInput;
20
+ }
21
+
22
+ toInputValue(value, $markoWebSearch) {
23
+ const { toInput, filter, validator } = this;
24
+ const defaultValue = this.getDefaultValue();
25
+
26
+ let input = isFn(toInput) ? toInput(value) : value;
27
+ if (input == null) input = defaultValue;
28
+ if (isFn(filter)) input = filter(input);
29
+ const isValid = isFn(validator) ? validator(input, $markoWebSearch) : true;
30
+ return isValid ? input : defaultValue;
31
+ }
32
+
33
+ toQueryValue(input, $markoWebSearch) {
34
+ const { fromInput, filter, validator } = this;
35
+ const defaultValue = this.getDefaultValue();
36
+
37
+ let v = input;
38
+ if (v == null) v = defaultValue;
39
+ if (isFn(filter)) v = filter(v);
40
+ const isValid = isFn(validator) ? validator(v, $markoWebSearch) : true;
41
+ if (!isValid) v = defaultValue;
42
+ if (this.isDefaultValue(v)) return null;
43
+ if (isFn(fromInput)) v = fromInput(v);
44
+ return v;
45
+ }
46
+
47
+ isDefaultValue(input) {
48
+ const defaultValue = this.getDefaultValue();
49
+ return this.areInputsEqual(input, defaultValue);
50
+ }
51
+
52
+ areInputsEqual(input1, input2) {
53
+ if (this.isArray()) {
54
+ return input1.sort().join('') === input2.sort().join('');
55
+ }
56
+ return input1 === input2;
57
+ }
58
+
59
+ getDefaultValue() {
60
+ const { defaultValue } = this;
61
+ return (isFn(defaultValue)) ? defaultValue() : defaultValue;
62
+ }
63
+
64
+ isArray() {
65
+ return this.type === Array;
66
+ }
67
+ }
68
+
69
+ module.exports = MarkoWebSearchQueryParam;
@@ -0,0 +1,101 @@
1
+ const MarkoWebSearchQueryParam = require('./param');
2
+
3
+ const { isArray } = Array;
4
+
5
+ const unqArray = (array) => [...new Set(array)];
6
+
7
+ const toArrayInput = (str, mapFunc) => {
8
+ if (!str) return undefined;
9
+ const trimmed = str.trim();
10
+ if (!trimmed) return undefined;
11
+ return unqArray(trimmed.split(',').map(mapFunc).filter((v) => v));
12
+ };
13
+
14
+ const fromArrayInput = (arr) => {
15
+ if (!isArray(arr) || !arr.length) return null;
16
+ return unqArray(arr.sort().filter((i) => i)).join(',');
17
+ };
18
+
19
+ const toIntArrayInput = (arr) => toArrayInput(arr, (v) => parseInt(v, 10));
20
+ const toStringArrayInput = (arr) => toArrayInput(arr, (v) => v.trim());
21
+
22
+ const sortFieldSet = new Set(['NAME', 'PUBLISHED', 'SCORE']);
23
+ const sortOrderSet = new Set(['DESC', 'ASC']);
24
+
25
+ class MarkoWebSearchQueryParamConfig {
26
+ constructor({
27
+ resultsPerPage = {},
28
+ contentTypeIds = [],
29
+ defaultSortField = 'PUBLISHED',
30
+ } = {}) {
31
+ this.params = new Map();
32
+
33
+ const contentTypeIdMap = contentTypeIds.reduce((m, id) => {
34
+ m.set(id, true);
35
+ return m;
36
+ }, new Map());
37
+
38
+ this
39
+ .add('limit', {
40
+ type: Number,
41
+ defaultValue: resultsPerPage.default,
42
+ validator: (v) => (v >= resultsPerPage.min && v <= resultsPerPage.max),
43
+ toInput: (v) => parseInt(v, 10),
44
+ })
45
+ .add('page', {
46
+ type: Number,
47
+ defaultValue: 1,
48
+ validator: (v, search) => {
49
+ if (v < 1) return false;
50
+ const limit = search.getInputValueFor('limit');
51
+ return ((v * limit) <= 10000);
52
+ },
53
+ toInput: (v) => parseInt(v, 10),
54
+ })
55
+ .add('searchQuery', {
56
+ type: String,
57
+ defaultValue: '',
58
+ })
59
+ .add('contentTypes', {
60
+ type: Array,
61
+ defaultValue: () => contentTypeIds.slice(),
62
+ filter: (types) => types.filter((type) => contentTypeIdMap.has(type)),
63
+ validator: (types) => types.every((type) => contentTypeIdMap.has(type)),
64
+ toInput: toStringArrayInput,
65
+ fromInput: fromArrayInput,
66
+ })
67
+ .add('assignedToWebsiteSectionIds', {
68
+ type: Array,
69
+ defaultValue: () => [],
70
+ toInput: toIntArrayInput,
71
+ fromInput: fromArrayInput,
72
+ })
73
+ .add('sortField', {
74
+ type: String,
75
+ defaultValue: defaultSortField,
76
+ validator: (v) => sortFieldSet.has(v),
77
+ })
78
+ .add('sortOrder', {
79
+ type: String,
80
+ defaultValue: 'DESC',
81
+ validator: (v) => sortOrderSet.has(v),
82
+ });
83
+ }
84
+
85
+ add(name, param) {
86
+ this.params.set(name, new MarkoWebSearchQueryParam({ ...param, name }));
87
+ return this;
88
+ }
89
+
90
+ names() {
91
+ return [...this.params.keys()].sort();
92
+ }
93
+
94
+ getDefinition(name) {
95
+ const param = this.params.get(name);
96
+ if (!param) throw new Error(`No query parameter definition was found for ${name}`);
97
+ return param;
98
+ }
99
+ }
100
+
101
+ module.exports = MarkoWebSearchQueryParamConfig;
package/index.js ADDED
@@ -0,0 +1,113 @@
1
+ class MarkoWebSearch {
2
+ constructor({ config, query = {} } = {}) {
3
+ this.config = config;
4
+ this.query = query;
5
+ this.input = this.getAllInputValues();
6
+ }
7
+
8
+ buildURLSearchParams(newValues = {}, omit = {}) {
9
+ return new URLSearchParams(this.queryParamNames.reduce((o, name) => {
10
+ if (omit[name]) return o;
11
+ const value = this.getQueryStringValueFor(name, newValues[name]);
12
+ if (value == null) return o;
13
+ return { ...o, [name]: value };
14
+ }, {}));
15
+ }
16
+
17
+ buildQueryString(newValues = {}, omit = {}) {
18
+ const params = this.buildURLSearchParams(newValues, omit);
19
+ const str = `${params}`;
20
+ return str ? `?${str}` : '';
21
+ }
22
+
23
+ /**
24
+ * Gets all component (internal) input values from the current Express request query.
25
+ *
26
+ * Invalid values are reset to the definition's default value.
27
+ * @returns {object}
28
+ */
29
+ getAllInputValues() {
30
+ return this.queryParamNames.reduce((o, name) => {
31
+ const value = this.getInputValueFor(name);
32
+ return { ...o, [name]: value };
33
+ }, {});
34
+ }
35
+
36
+ /**
37
+ * Gets the component (internal) input value for the provided parameter name and
38
+ * current Express request query.
39
+ *
40
+ * Invalid values are reset to the definition's default value.
41
+ *
42
+ * @param {string} name The query parameter name.
43
+ * @param {object} query The Express request query object.
44
+ * @returns {*}
45
+ */
46
+ getInputValueFor(name) {
47
+ const definition = this.config.queryParams.getDefinition(name);
48
+ return definition.toInputValue(this.query[name], this);
49
+ }
50
+
51
+ isInputValueSelectedFor(name, incomingInput) {
52
+ const definition = this.config.queryParams.getDefinition(name);
53
+ const currentInput = this.getInputValueFor(name);
54
+ return definition.areInputsEqual(currentInput, incomingInput);
55
+ }
56
+
57
+ isDefaultInputValueFor(name) {
58
+ const definition = this.config.queryParams.getDefinition(name);
59
+ const input = this.getInputValueFor(name);
60
+ return definition.isDefaultValue(input);
61
+ }
62
+
63
+ isArrayParam(name) {
64
+ const definition = this.config.queryParams.getDefinition(name);
65
+ return definition.isArray();
66
+ }
67
+
68
+ /**
69
+ *
70
+ * @param {string} name The query parameter name
71
+ * @param {*} [newValue] The optional new value to replace
72
+ * @returns {string?}
73
+ */
74
+ getQueryStringValueFor(name, newValue) {
75
+ const definition = this.config.queryParams.getDefinition(name);
76
+ const value = typeof newValue === 'undefined' ? this.input[name] : newValue;
77
+ return definition.toQueryValue(value, this);
78
+ }
79
+
80
+ get queryParamNames() {
81
+ return this.config.queryParams.names();
82
+ }
83
+
84
+ getCurrentPage() {
85
+ return this.getInputValueFor('page');
86
+ }
87
+
88
+ getLimit() {
89
+ return this.getInputValueFor('limit');
90
+ }
91
+
92
+ getSkip() {
93
+ return this.getLimit() * (this.getCurrentPage() - 1);
94
+ }
95
+
96
+ getTotalPages(totalCount = 0) {
97
+ const count = totalCount > 10000 ? 10000 : totalCount;
98
+ return Math.ceil(count / this.getLimit());
99
+ }
100
+
101
+ getNextPage(totalCount = 0) {
102
+ const page = this.getCurrentPage();
103
+ const total = this.getTotalPages(totalCount);
104
+ return page < total ? page + 1 : null;
105
+ }
106
+
107
+ getPrevPage() {
108
+ const page = this.getCurrentPage();
109
+ return page > 1 ? page - 1 : null;
110
+ }
111
+ }
112
+
113
+ module.exports = MarkoWebSearch;
@@ -0,0 +1,108 @@
1
+ const gql = require('graphql-tag');
2
+ const { extractFragmentData } = require('@mindful-web/web-common/utils');
3
+
4
+ const buildMindfulWebQuery = ({ queryFragment, opSuffix = '' } = {}) => {
5
+ const { spreadFragmentName, processedFragment } = extractFragmentData(queryFragment);
6
+ return gql`
7
+ query MarkoWebSearchContentIdsFromBrowse${opSuffix}($ids: [Int!]!, $limit: Int!) {
8
+ allContent(input: { ids: $ids, pagination: { limit: $limit } }) {
9
+ edges {
10
+ node {
11
+ id
12
+ ${spreadFragmentName}
13
+ }
14
+ }
15
+ }
16
+ }
17
+ ${processedFragment}
18
+ `;
19
+ };
20
+
21
+ const baseBrowseQuery = gql`
22
+ query MarkoWebSearchSearchContentIds($input: BrowseContentQueryInput!) {
23
+ searchContentIds(input: $input) {
24
+ totalCount
25
+ pageInfo {
26
+ hasNextPage
27
+ hasPreviousPage
28
+ }
29
+ ids
30
+ }
31
+ }
32
+ `;
33
+
34
+ /**
35
+ * @param {object} clients
36
+ * @param {ApolloClient} clients.apolloMindfulWebCMS The Mindful Web client that will perform the
37
+ * query.
38
+ * @param {ApolloClient} clients.apolloBaseBrowse The BaseBrowse client that will perform the query.
39
+ * @param {object} params
40
+ * @param {number} [params.limit]
41
+ * @param {number} [params.skip]
42
+ * @param {string} [params.sortField=PUBLISHED]
43
+ * @param {string} [params.sortOrder=DESC]
44
+ * @param {string} [params.searchQuery]
45
+ * @param {string[]} [params.contentTypes]
46
+ * @param {string[]} [params.countryCodes]
47
+ * @param {string[]} [params.assignedToWebsiteSiteIds]
48
+ * @param {number[]} [params.assignedToWebsiteSectionIds]
49
+ * @param {string} [params.queryFragment] The `graphql-tag` fragment
50
+ * to apply to the `allContent` query.
51
+ * @param {string} [params.opSuffix] A suffix to add to the GraphQL operation name.
52
+ */
53
+ module.exports = async ({ apolloMindfulWebCMS, apolloBaseBrowse } = {}, {
54
+ limit,
55
+ skip,
56
+
57
+ sortField = 'PUBLISHED',
58
+ sortOrder = 'DESC',
59
+
60
+ searchQuery,
61
+ contentTypes = [],
62
+ countryCodes = [],
63
+ assignedToWebsiteSiteIds = [],
64
+ assignedToWebsiteSectionIds = [],
65
+
66
+ queryFragment,
67
+ opSuffix,
68
+ searchType,
69
+ } = {}) => {
70
+ if (!apolloMindfulWebCMS || !apolloBaseBrowse) throw new Error('Both the Mindful Web and Base Browse Apollo clients must be provided.');
71
+ const input = {
72
+ omitScheduledAndExpiredContent: true,
73
+ statuses: ['PUBLISHED'],
74
+ contentTypes,
75
+ countryCodes,
76
+ ...(searchQuery && { search: { query: searchQuery, type: searchType } }),
77
+ ...((assignedToWebsiteSiteIds.length || assignedToWebsiteSectionIds.length) && {
78
+ assignedToWebsites: {
79
+ ...(assignedToWebsiteSiteIds.length && { siteIds: assignedToWebsiteSiteIds }),
80
+ ...(assignedToWebsiteSectionIds.length && { sectionIds: assignedToWebsiteSectionIds }),
81
+ },
82
+ }),
83
+ pagination: { limit, skip },
84
+ sort: {
85
+ field: sortField,
86
+ order: sortOrder,
87
+ },
88
+ };
89
+
90
+ const { data: baseBrowseData } = await apolloBaseBrowse.query({
91
+ query: baseBrowseQuery,
92
+ variables: { input },
93
+ });
94
+
95
+ const { ids, pageInfo, totalCount } = baseBrowseData.searchContentIds;
96
+ if (!ids.length) return { nodes: [], pageInfo, totalCount };
97
+
98
+ const { data } = await apolloMindfulWebCMS.query({
99
+ query: buildMindfulWebQuery({ queryFragment, opSuffix }),
100
+ variables: { ids, limit: ids.length },
101
+ });
102
+ const nodes = data.allContent.edges
103
+ .map((edge) => (edge && edge.node ? edge.node : null))
104
+ .filter((c) => c);
105
+ const map = nodes.reduce((m, node) => m.set(`${node.id}`, node), new Map());
106
+ const ordered = ids.map((id) => map.get(`${id}`)).filter((node) => node);
107
+ return { nodes: ordered, pageInfo, totalCount };
108
+ };
package/marko.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "taglib-id": "@mindful-web/marko-search",
3
+ "taglib-imports": [
4
+ "./components/marko.json"
5
+ ]
6
+ }
package/middleware.js ADDED
@@ -0,0 +1,9 @@
1
+ const MarkoWebSearch = require('./index');
2
+
3
+ module.exports = ({ config, template } = {}) => (req, res) => {
4
+ res.locals.$markoWebSearch = new MarkoWebSearch({
5
+ config,
6
+ query: req.query,
7
+ });
8
+ return res.marko(template);
9
+ };
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@mindful-web/marko-web-search",
3
+ "version": "1.0.0",
4
+ "author": "Jacob Bare <jacob@parameter1.com>",
5
+ "repository": "https://github.com/parameter1/mindful-web/tree/main/packages/marko-web-search",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "lint:fix": "yarn lint --fix",
9
+ "lint": "eslint --ext .js --ext .vue --max-warnings 5 ./",
10
+ "compile": "mindful-web-marko-compile compile",
11
+ "prepublish": "yarn compile --silent",
12
+ "test": "yarn compile --no-clean && yarn lint"
13
+ },
14
+ "dependencies": {
15
+ "@mindful-web/inflector": "^1.0.0",
16
+ "@mindful-web/marko-web-icons": "^1.0.0",
17
+ "@mindful-web/object-path": "^1.0.0",
18
+ "@mindful-web/utils": "^1.0.0",
19
+ "@mindful-web/web-common": "^1.0.0",
20
+ "@parameter1/joi": "^1.2.10",
21
+ "graphql": "^14.7.0",
22
+ "graphql-tag": "^2.12.6"
23
+ },
24
+ "peerDependencies": {
25
+ "@mindful-web/marko-core": "^0.0.0",
26
+ "@mindful-web/marko-web": "^0.0.0"
27
+ },
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "gitHead": "0b77cab713eb5841202bb86c7119949866bc68b5"
32
+ }