@performant-software/shared-components 0.5.1

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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +0 -0
  3. package/build/index.js +2 -0
  4. package/build/index.js.map +1 -0
  5. package/build/main.css +11 -0
  6. package/index.js +1 -0
  7. package/package.json +36 -0
  8. package/src/api/Attachments.js +28 -0
  9. package/src/api/BaseService.js +127 -0
  10. package/src/api/BaseTransform.js +55 -0
  11. package/src/api/FormDataTransform.js +30 -0
  12. package/src/api/NestedAttributesTransform.js +63 -0
  13. package/src/components/EditContainer.css +0 -0
  14. package/src/components/EditContainer.js +448 -0
  15. package/src/components/GoogleAnalytics.css +0 -0
  16. package/src/components/GoogleAnalytics.js +118 -0
  17. package/src/components/GoogleScript.js +5 -0
  18. package/src/components/InfiniteScroll.css +1 -0
  19. package/src/components/InfiniteScroll.js +120 -0
  20. package/src/components/Keyboard.css +11 -0
  21. package/src/components/Keyboard.js +55 -0
  22. package/src/i18n/en.json +204 -0
  23. package/src/i18n/i18n.js +24 -0
  24. package/src/index.js +34 -0
  25. package/src/utils/Browser.js +8 -0
  26. package/src/utils/Calendar.js +232 -0
  27. package/src/utils/Date.js +10 -0
  28. package/src/utils/DragDrop.js +17 -0
  29. package/src/utils/Element.js +36 -0
  30. package/src/utils/Map.js +27 -0
  31. package/src/utils/Object.js +114 -0
  32. package/src/utils/String.js +20 -0
  33. package/src/utils/Timer.js +32 -0
  34. package/src/utils/Utility.js +14 -0
  35. package/test/api/Attachments.spec.js +32 -0
  36. package/types/api/Attachments.js.flow +28 -0
  37. package/types/api/BaseService.js.flow +127 -0
  38. package/types/api/BaseTransform.js.flow +55 -0
  39. package/types/api/FormDataTransform.js.flow +30 -0
  40. package/types/api/NestedAttributesTransform.js.flow +63 -0
  41. package/types/components/EditContainer.js.flow +448 -0
  42. package/types/components/GoogleAnalytics.js.flow +118 -0
  43. package/types/components/GoogleScript.js.flow +5 -0
  44. package/types/components/InfiniteScroll.js.flow +120 -0
  45. package/types/components/Keyboard.js.flow +55 -0
  46. package/types/i18n/i18n.js.flow +24 -0
  47. package/types/index.js.flow +34 -0
  48. package/types/utils/Browser.js.flow +8 -0
  49. package/types/utils/Calendar.js.flow +232 -0
  50. package/types/utils/Date.js.flow +10 -0
  51. package/types/utils/DragDrop.js.flow +17 -0
  52. package/types/utils/Element.js.flow +36 -0
  53. package/types/utils/Map.js.flow +27 -0
  54. package/types/utils/Object.js.flow +114 -0
  55. package/types/utils/String.js.flow +20 -0
  56. package/types/utils/Timer.js.flow +32 -0
  57. package/types/utils/Utility.js.flow +14 -0
  58. package/webpack.config.js +3 -0
@@ -0,0 +1,114 @@
1
+ // @flow
2
+
3
+ import _ from 'underscore';
4
+
5
+ type OptionsProps = {
6
+ emptyValues: Array<any>,
7
+ ignoreHtml: boolean
8
+ };
9
+
10
+ const EMPTY_VALUES = [
11
+ '',
12
+ null,
13
+ undefined,
14
+ [],
15
+ {}
16
+ ];
17
+
18
+ const DEFAULT_OPTIONS = {
19
+ emptyValues: EMPTY_VALUES,
20
+ ignoreHtml: true,
21
+ ignoreWhitespace: true
22
+ };
23
+
24
+ const HTML_REGEX = /(<([^>]+)>)/gi;
25
+ const WHITESPACE_REGEX = /\s\s+/g;
26
+
27
+ /**
28
+ * Returns true if the passed value is considered "empty".
29
+ *
30
+ * @param value
31
+ *
32
+ * @returns {boolean|*}
33
+ */
34
+ export const isEmpty = (value: any) => {
35
+ // If the value is an object or array, use underscore's isEmpty check.
36
+ if (_.isObject(value) || _.isArray(value)) {
37
+ return _.isEmpty(value);
38
+ }
39
+
40
+ return !value;
41
+ };
42
+
43
+ /**
44
+ * Returns true if the passed two arguments as deep equal. This function will perform a recursive check against all of
45
+ * the keys in the objects.
46
+ *
47
+ * @param a
48
+ * @param b
49
+ *
50
+ * @returns {boolean}
51
+ */
52
+ export const isEqual = (a: any, b: any, userOptions: OptionsProps = {}) => {
53
+ const options = _.defaults(userOptions, DEFAULT_OPTIONS);
54
+
55
+ // Check equality, consider empty strings and "null" values as equal
56
+ if (a === b || (_.contains(options.emptyValues, a) && _.contains(options.emptyValues, b))) {
57
+ return true;
58
+ }
59
+
60
+ // Deep string comparison
61
+ if (_.isString(a) && _.isString(b)) {
62
+ let aString = a;
63
+ let bString = b;
64
+
65
+ // Remove superfluous whitespace
66
+ if (options.ignoreWhitespace) {
67
+ aString = a.replace(WHITESPACE_REGEX, ' ');
68
+ bString = b.replace(WHITESPACE_REGEX, ' ');
69
+ }
70
+
71
+ // If we're ignoring HTML, compare the string values with HTML tags removed
72
+ if (options.ignoreHtml) {
73
+ aString = aString.replace(HTML_REGEX, '');
74
+ bString = bString.replace(HTML_REGEX, '');
75
+ }
76
+
77
+ if (aString === bString) {
78
+ return true;
79
+ }
80
+ }
81
+
82
+ if (a !== null && typeof a === 'object' && b !== null && typeof b === 'object') {
83
+ const aKeys = _.keys(a);
84
+ const bKeys = _.keys(b);
85
+
86
+ // If the objects contain different number of keys, return false
87
+ if (aKeys.length !== bKeys.length) {
88
+ return false;
89
+ }
90
+
91
+ // Recursively check each key for equality
92
+ let equal = true;
93
+
94
+ _.each(_.keys(a), (key) => {
95
+ if (!(_.has(b, key) && isEqual(a[key], b[key]))) {
96
+ equal = false;
97
+ }
98
+ });
99
+
100
+ // If any of the recursive keys are not equal, return false
101
+ if (!equal) {
102
+ return false;
103
+ }
104
+
105
+ // If we've made it this far, we've checked the equality of all the keys in both objects, return true
106
+ return true;
107
+ }
108
+
109
+ return false;
110
+ };
111
+
112
+ export default {
113
+
114
+ };
@@ -0,0 +1,20 @@
1
+ // @flow
2
+
3
+ import _ from 'underscore';
4
+
5
+ const includes = (a: any, b: any) => (
6
+ a && b && a.toString().toLowerCase().includes(b.toString().toLowerCase())
7
+ );
8
+
9
+ const toString = (value: any) => {
10
+ if (_.isNumber(value) || _.isBoolean(value)) {
11
+ return value;
12
+ }
13
+
14
+ return value || '';
15
+ };
16
+
17
+ export default {
18
+ includes,
19
+ toString
20
+ };
@@ -0,0 +1,32 @@
1
+ // @flow
2
+
3
+ const DEFAULT_TIMEOUT = 500;
4
+
5
+ /**
6
+ * The timer class encapsulates the logic for setting and clearing a timeout. This is particularly useful for
7
+ * keydown/keyup events when we only want to perform an action after the user has finished typing.
8
+ */
9
+ class Timer {
10
+ timeout: TimeoutID | null;
11
+
12
+ constructor() {
13
+ this.timeout = null;
14
+ }
15
+
16
+ /**
17
+ * Clears the search timer.
18
+ */
19
+ clearSearchTimer() {
20
+ clearTimeout(this.timeout);
21
+ }
22
+
23
+ /**
24
+ * Sets the search timer.
25
+ */
26
+ setSearchTimer(onTimeout: () => void) {
27
+ clearTimeout(this.timeout);
28
+ this.timeout = setTimeout(onTimeout, DEFAULT_TIMEOUT);
29
+ }
30
+ }
31
+
32
+ export default new Timer();
@@ -0,0 +1,14 @@
1
+ // @flow
2
+
3
+ /**
4
+ * Returns true if passed value is a promise (i.e, has a .then method)
5
+ *
6
+ * @param *
7
+ *
8
+ * @returns {boolean}
9
+ */
10
+ const isPromise = (value: any) => !!value && typeof value === 'object' && typeof value.then === 'function';
11
+
12
+ export default {
13
+ isPromise
14
+ };
@@ -0,0 +1,32 @@
1
+ import Attachments from '../../src/api/Attachments';
2
+
3
+ describe('toPayload', () => {
4
+ test('toPayload adds the attachment, if provided', () => {
5
+ const formData = {
6
+ append: jest.fn()
7
+ };
8
+
9
+ const record = {
10
+ attachment: jest.fn(),
11
+ attachment_remove: true
12
+ };
13
+
14
+ Attachments.toPayload(formData, 'test', record, 'attachment');
15
+
16
+ expect(formData.append).toHaveBeenCalledWith('test[attachment]', record.attachment);
17
+ });
18
+
19
+ test('toPayload removes the attachment if the *_remove attribute is provided with no attachment', () => {
20
+ const formData = {
21
+ append: jest.fn()
22
+ };
23
+
24
+ const record = {
25
+ attachment_remove: true
26
+ };
27
+
28
+ Attachments.toPayload(formData, 'test', record, 'attachment');
29
+
30
+ expect(formData.append).toHaveBeenCalledWith('test[attachment_remove]', true);
31
+ });
32
+ });
@@ -0,0 +1,28 @@
1
+ // @flow
2
+
3
+ /**
4
+ * Helper class for handling binary data. This class should be used in conjunction with the FormDataTransform.
5
+ */
6
+ class Attachments {
7
+ /**
8
+ * Appends the attachment for the passed record to the passed form data. If the attachment is not present, the
9
+ * "*_remove" attribute will be appended if the attachment has been removed.
10
+ *
11
+ * @param formData
12
+ * @param prefix
13
+ * @param record
14
+ * @param name
15
+ */
16
+ toPayload(formData: FormData, prefix: string, record: any, name: string) {
17
+ const attachment = record[name];
18
+ const removeAttribute = `${name}_remove`;
19
+
20
+ if (attachment) {
21
+ formData.append(`${prefix}[${name}]`, attachment);
22
+ } else if (record[removeAttribute]) {
23
+ formData.append(`${prefix}[${removeAttribute}]`, record[removeAttribute]);
24
+ }
25
+ }
26
+ }
27
+
28
+ export default new Attachments();
@@ -0,0 +1,127 @@
1
+ // @flow
2
+
3
+ import axios, { type AxiosResponse } from 'axios';
4
+
5
+ /**
6
+ * Base class for making API calls. This class uses Axios under the hood and a customizable transform class for
7
+ * PUT/POST requests.
8
+ */
9
+ class BaseService {
10
+ /**
11
+ * Constructs a new BaseService object. This constructor should never be used directly.
12
+ */
13
+ constructor() {
14
+ if (this.constructor === BaseService) {
15
+ throw new TypeError('Abstract class "BaseService" cannot be instantiated directly.');
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Calls the POST /api/<resource>/ endpoint with the passed item.
21
+ *
22
+ * @param item
23
+ *
24
+ * @returns {Promise<AxiosResponse<T>>}
25
+ */
26
+ create(item: any): Promise<AxiosResponse> {
27
+ const transform = this.getTransform();
28
+
29
+ // $FlowFixMe - Flow doesn't currently support abstract classes
30
+ return axios.post(this.getBaseUrl(), transform.toPayload(item), this.getConfig());
31
+ }
32
+
33
+ /**
34
+ * Calls the DELETE /api/<resource>/:id endpoint for the passed item.
35
+ *
36
+ * @param item
37
+ *
38
+ * @returns {Promise<AxiosResponse<T>>}
39
+ */
40
+ delete(item: any) {
41
+ return axios.delete(`${this.getBaseUrl()}/${item.id}`);
42
+ }
43
+
44
+ /**
45
+ * Calls the GET /api/<resource>/ endpoint.
46
+ *
47
+ * @returns {Promise<AxiosResponse<T>>}
48
+ */
49
+ fetchAll(params: any) {
50
+ return axios.get(this.getBaseUrl(), { params });
51
+ }
52
+
53
+ /**
54
+ * Calls the GET /api/<resource>/:id endpoint.
55
+ *
56
+ * @returns {Promise<AxiosResponse<T>>}
57
+ */
58
+ fetchOne(id: number) {
59
+ return axios.get(`${this.getBaseUrl()}/${id}`);
60
+ }
61
+
62
+ /**
63
+ * Calls the create/update API endpoint for the passed item.
64
+ *
65
+ * @param item
66
+ *
67
+ * @returns {Promise<AxiosResponse<T>>}
68
+ */
69
+ save(item: any) {
70
+ return item.id ? this.update(item) : this.create(item);
71
+ }
72
+
73
+ /**
74
+ * Calls the POST /api/<resource>/search endpoint.
75
+ *
76
+ * @param params
77
+ *
78
+ * @returns {Promise<AxiosResponse<T>>}
79
+ */
80
+ search(params: any) {
81
+ return axios.post(`${this.getBaseUrl()}/search`, params);
82
+ }
83
+
84
+ /**
85
+ * Calls the PUT /api/<resource>/:id endpoint with the passed item.
86
+ *
87
+ * @param item
88
+ *
89
+ * @returns {Promise<AxiosResponse<T>>}
90
+ */
91
+ update(item: any) {
92
+ const transform = this.getTransform();
93
+
94
+ // $FlowFixMe - Flow doesn't currently support abstract classes
95
+ return axios.put(`${this.getBaseUrl()}/${item.id}`, transform.toPayload(item), this.getConfig());
96
+ }
97
+
98
+ // protected
99
+
100
+ /**
101
+ * Returns the API base URL string.
102
+ */
103
+ getBaseUrl(): string {
104
+ // Implemented in concrete classes.
105
+ return '';
106
+ }
107
+
108
+ /**
109
+ * Returns the config properties for POST/PUT requests.
110
+ *
111
+ * @returns {null}
112
+ */
113
+ getConfig() {
114
+ // Implemented in concrete classes
115
+ return null;
116
+ }
117
+
118
+ /**
119
+ * Returns the transform object. This class will be used to generate the object sent on POST/PUT requests.
120
+ */
121
+ getTransform() {
122
+ // Implemented in concrete classes.
123
+ return {};
124
+ }
125
+ }
126
+
127
+ export default BaseService;
@@ -0,0 +1,55 @@
1
+ // @flow
2
+
3
+ import _ from 'underscore';
4
+
5
+ /**
6
+ * Class for handling transforming objects for PUT/POST requests. This class transforms records into
7
+ * plain Javascript objects.
8
+ */
9
+ class BaseTransform {
10
+ /**
11
+ * Constructs a new BaseTransform object. This constructor should never be used directly.
12
+ */
13
+ constructor() {
14
+ if (this.constructor === BaseTransform) {
15
+ throw new TypeError('Abstract class "BaseTransform" cannot be instantiated directly.');
16
+ }
17
+ }
18
+
19
+ // protected
20
+
21
+ /**
22
+ * Returns the parameter name.
23
+ *
24
+ * @returns {string}
25
+ */
26
+ getParameterName(): string {
27
+ // Implemented in sub-class
28
+ return '';
29
+ }
30
+
31
+ /**
32
+ * Returns the array of payload keys.
33
+ *
34
+ * @returns {*[]}
35
+ */
36
+ getPayloadKeys(): Array<string> {
37
+ // Implemented in sub-class
38
+ return [];
39
+ }
40
+
41
+ /**
42
+ * Returns the object for POST/PUT requests as a plain Javascript object.
43
+ *
44
+ * @param item
45
+ *
46
+ * @returns any
47
+ */
48
+ toPayload(item: any): any {
49
+ return {
50
+ [this.getParameterName()]: _.pick(item, this.getPayloadKeys())
51
+ };
52
+ }
53
+ }
54
+
55
+ export default BaseTransform;
@@ -0,0 +1,30 @@
1
+ // @flow
2
+
3
+ import _ from 'underscore';
4
+ import BaseTransform from './BaseTransform';
5
+ import StringUtils from '../utils/String';
6
+
7
+ /**
8
+ * Class for handling transforming records for PUT/POST requests. This class transforms objects in FormData. This
9
+ * class is useful if your model contains binary data to be uploaded.
10
+ */
11
+ class FormDataTransform extends BaseTransform {
12
+ /**
13
+ * Converts the passed records to a formData object to be sent on PUT/POST requests.
14
+ *
15
+ * @param record
16
+ *
17
+ * @returns {FormData}
18
+ */
19
+ toPayload(record: any) {
20
+ const formData = new FormData();
21
+
22
+ _.each(this.getPayloadKeys(), (key) => {
23
+ formData.append(`${this.getParameterName()}[${key}]`, StringUtils.toString(record[key]));
24
+ });
25
+
26
+ return formData;
27
+ }
28
+ }
29
+
30
+ export default FormDataTransform;
@@ -0,0 +1,63 @@
1
+ // @flow
2
+
3
+ import _ from 'underscore';
4
+ import StringUtils from '../utils/String';
5
+
6
+ /**
7
+ * Class for handling transforming nested attributes of a parent object. This class will handle transforming the
8
+ * object into a payload to be sent to a POST/PUT request on an API. This class currently supports transforming into
9
+ * a plain Javascript object or a FormData object.
10
+ */
11
+ class NestedAttributesTransform {
12
+ /**
13
+ * Constructs a new BaseTransform object. This constructor should never be used directly.
14
+ */
15
+ constructor() {
16
+ if (this.constructor === NestedAttributesTransform) {
17
+ throw new TypeError('Abstract class "NestedAttributesTransform" cannot be instantiated directly.');
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Returns the array of payload keys.
23
+ *
24
+ * @returns {*[]}
25
+ */
26
+ getPayloadKeys() {
27
+ // Implemented in sub-class.
28
+ return [];
29
+ }
30
+
31
+ /**
32
+ * Appends the passed record's collection to the form data.
33
+ *
34
+ * @param formData
35
+ * @param prefix
36
+ * @param record
37
+ * @param collection
38
+ */
39
+ toFormData(formData: FormData, prefix: string, record: any, collection: string) {
40
+ _.each(record[collection], (item, index) => {
41
+ _.each(this.getPayloadKeys(), (key) => {
42
+ formData.append(`${prefix}[${collection}][${index}][${key}]`, StringUtils.toString(item[key]));
43
+ });
44
+ });
45
+ }
46
+
47
+ /**
48
+ * Transforms the passed record's collection to a payload object.
49
+ *
50
+ * @param record
51
+ * @param collection
52
+ *
53
+ * @returns {{[p: string]: *}}
54
+ */
55
+ toPayload(record: any, collection: string) {
56
+ return {
57
+ [collection]: _.map(record[collection],
58
+ (item, index) => ({ ..._.pick(item, this.getPayloadKeys()), order: index }))
59
+ };
60
+ }
61
+ }
62
+
63
+ export default NestedAttributesTransform;