@startinblox/boilerplate 3.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 (54) hide show
  1. package/.gitlab-ci.yml +57 -0
  2. package/.storybook/main.ts +15 -0
  3. package/.storybook/preview-head.html +8 -0
  4. package/.storybook/preview.ts +22 -0
  5. package/LICENSE +21 -0
  6. package/README.md +85 -0
  7. package/biome.json +39 -0
  8. package/cypress/component/solid-boilerplate.cy.ts +9 -0
  9. package/cypress/cypress.d.ts +1 -0
  10. package/cypress/support/component-index.html +12 -0
  11. package/cypress/support/component.ts +17 -0
  12. package/cypress.config.ts +11 -0
  13. package/dist/boilerplate.css +1 -0
  14. package/dist/index.js +1213 -0
  15. package/lit-localize.json +15 -0
  16. package/locales/en.xlf +13 -0
  17. package/package.json +92 -0
  18. package/postcss.config.js +8 -0
  19. package/src/component.d.ts +161 -0
  20. package/src/components/solid-boilerplate.ts +79 -0
  21. package/src/components/ui/sample-object.ts +37 -0
  22. package/src/components/ui/sample-objects.ts +40 -0
  23. package/src/context.json +1 -0
  24. package/src/helpers/components/componentObjectHandler.ts +100 -0
  25. package/src/helpers/components/componentObjectsHandler.ts +44 -0
  26. package/src/helpers/components/orbitComponent.ts +241 -0
  27. package/src/helpers/components/setupCacheInvalidation.ts +37 -0
  28. package/src/helpers/components/setupCacheOnResourceReady.ts +32 -0
  29. package/src/helpers/components/setupComponentSubscriptions.ts +73 -0
  30. package/src/helpers/components/setupOnSaveReset.ts +20 -0
  31. package/src/helpers/datas/dataBuilder.ts +43 -0
  32. package/src/helpers/datas/filterGenerator.ts +29 -0
  33. package/src/helpers/datas/filterObjectByDateAfter.ts +80 -0
  34. package/src/helpers/datas/filterObjectById.ts +54 -0
  35. package/src/helpers/datas/filterObjectByInterval.ts +133 -0
  36. package/src/helpers/datas/filterObjectByNamedValue.ts +103 -0
  37. package/src/helpers/datas/filterObjectByType.ts +30 -0
  38. package/src/helpers/datas/filterObjectByValue.ts +81 -0
  39. package/src/helpers/datas/sort.ts +40 -0
  40. package/src/helpers/i18n/configureLocalization.ts +17 -0
  41. package/src/helpers/index.ts +41 -0
  42. package/src/helpers/ui/formatDate.ts +18 -0
  43. package/src/helpers/ui/lipsum.ts +12 -0
  44. package/src/helpers/utils/requestNavigation.ts +12 -0
  45. package/src/helpers/utils/uniq.ts +6 -0
  46. package/src/index.ts +7 -0
  47. package/src/initializer.ts +11 -0
  48. package/src/mocks/orbit.mock.ts +33 -0
  49. package/src/mocks/user.mock.ts +67 -0
  50. package/src/styles/component-sample.scss +4 -0
  51. package/src/styles/index.scss +16 -0
  52. package/stories/sample-objects.stories.ts +47 -0
  53. package/tsconfig.json +36 -0
  54. package/vite.config.ts +44 -0
@@ -0,0 +1,241 @@
1
+ import { nothing } from "lit";
2
+ import { property, state } from "lit/decorators.js";
3
+
4
+ import {
5
+ CLIENT_CONTEXT,
6
+ requestNavigation,
7
+ setupComponentSubscriptions,
8
+ } from "@helpers";
9
+
10
+ import { ComponentObjectsHandler } from "@helpers/components/componentObjectsHandler";
11
+
12
+ import type {
13
+ Container,
14
+ LiveOrbit,
15
+ PropertiesPicker,
16
+ ProxyValue,
17
+ Resource,
18
+ UnknownResource,
19
+ } from "@src/component";
20
+
21
+ export default class extends ComponentObjectsHandler {
22
+ constructor({
23
+ defaultRoute = false,
24
+ setupSubscriptions = true,
25
+ ignoreRouter = false,
26
+ } = {}) {
27
+ super();
28
+ const attach = () => {
29
+ this._attach(defaultRoute, setupSubscriptions, ignoreRouter).then(
30
+ (attach: boolean) => {
31
+ if (attach) {
32
+ this.requestUpdate();
33
+ }
34
+ },
35
+ );
36
+ };
37
+ if (document.readyState === "loading") {
38
+ document.addEventListener("DOMContentLoaded", attach);
39
+ } else {
40
+ attach();
41
+ }
42
+ }
43
+
44
+ @property({ attribute: "default-data-src", reflect: true })
45
+ defaultDataSrc?: string;
46
+
47
+ @property({ attribute: "data-src", reflect: true })
48
+ dataSrc?: string;
49
+
50
+ @property({ attribute: "nested-field" })
51
+ nestedField?: string;
52
+
53
+ @property({ attribute: "uniq" })
54
+ uniq?: string;
55
+
56
+ @property({ attribute: "route" })
57
+ route: string | undefined;
58
+
59
+ @property({ attribute: false })
60
+ cherryPickedProperties: PropertiesPicker[] = [];
61
+
62
+ @state()
63
+ orbit: LiveOrbit | undefined;
64
+
65
+ @state()
66
+ currentRoute = "";
67
+
68
+ protected async _attach(
69
+ defaultRoute: boolean,
70
+ setupSubscriptions: boolean,
71
+ ignoreRouter: boolean
72
+ ) {
73
+ if (!this.orbit) {
74
+ if (window.orbit) {
75
+ this.orbit = window.orbit;
76
+ if (setupSubscriptions) {
77
+ setupComponentSubscriptions({
78
+ component: this,
79
+ defaultRoute: defaultRoute,
80
+ ignoreRouter: ignoreRouter,
81
+ });
82
+ if (this.route) {
83
+ this.component = this.orbit.getComponentFromRoute(this.route);
84
+ if (this.component) {
85
+ this.orbit.components.map((c) => {
86
+ if (c.uniq === this.component.uniq) {
87
+ c.instance = this;
88
+ }
89
+ return c;
90
+ });
91
+ }
92
+ }
93
+ await this._afterAttach();
94
+ return Promise.resolve(true);
95
+ }
96
+ }
97
+ }
98
+ return Promise.resolve(false);
99
+ }
100
+
101
+ async _afterAttach() {
102
+ return Promise.resolve();
103
+ }
104
+
105
+ _navigate(e: Event) {
106
+ window.sibRouter.previousRoute = this.currentRoute;
107
+ window.sibRouter.previousResource = window.sibRouter.currentResource;
108
+ const navigator = e.target?.closest("[navigation-target]");
109
+ let target = navigator.getAttribute("navigation-target");
110
+ const subrouter = navigator.getAttribute("navigation-subrouter");
111
+ const resource = navigator.getAttribute("navigation-resource");
112
+ const rdfType = navigator.getAttribute("navigation-rdf-type");
113
+ if (rdfType) {
114
+ const compatibleComponents = window.orbit?.components?.filter(
115
+ (c) => c?.routeAttributes?.["rdf-type"] === rdfType
116
+ );
117
+ if (compatibleComponents) target = compatibleComponents[0]?.uniq;
118
+ }
119
+ if (target) {
120
+ requestNavigation(
121
+ (window.orbit ? window.orbit.getRoute(target, true) : target) +
122
+ (subrouter ? `-${subrouter}` : ""),
123
+ resource
124
+ );
125
+ }
126
+ e.preventDefault();
127
+ }
128
+
129
+ _normalizeLdpContains(value: Resource[] | Resource): Resource[] {
130
+ if (!Array.isArray(value) && value !== null) {
131
+ return [value];
132
+ }
133
+ return value;
134
+ }
135
+
136
+ async _expandContainer(
137
+ value: Resource[],
138
+ recursive = true
139
+ ): Promise<UnknownResource[]> {
140
+ const expandedContainer: UnknownResource[] = [];
141
+ for (const entry of value) {
142
+ const line = await this._getProxyValue(entry, recursive);
143
+ if (line) expandedContainer.push(line);
144
+ }
145
+ return expandedContainer;
146
+ }
147
+
148
+ async _getProperties(
149
+ resource: Resource,
150
+ recursive = true,
151
+ targetProperties: PropertiesPicker[] = this.cherryPickedProperties
152
+ ) {
153
+ const properties = await resource.properties;
154
+ const response: Resource = {
155
+ "@id": resource["@id"],
156
+ "@type": resource["@type"],
157
+ "@context": resource.serverContext,
158
+ _originalResource: resource,
159
+ };
160
+ for (const prop of targetProperties) {
161
+ if (properties?.includes(prop.key)) {
162
+ response[prop.value] = await resource.get(prop.key);
163
+ if (prop.expand) {
164
+ response[prop.value] = await this._getProxyValue(
165
+ response[prop.value],
166
+ recursive,
167
+ targetProperties
168
+ );
169
+ }
170
+ if (prop.cast) {
171
+ response[prop.value] = prop.cast(response[prop.value]);
172
+ }
173
+ }
174
+ }
175
+ return await this._responseAdaptator(response);
176
+ }
177
+
178
+ async _getProxyValue(
179
+ resource:
180
+ | string
181
+ | Resource
182
+ | ProxyValue<Resource | Container<ProxyValue<Resource> | Resource>>,
183
+ recursive = true,
184
+ targetProperties: PropertiesPicker[] = this.cherryPickedProperties
185
+ ) {
186
+ try {
187
+ if (resource) {
188
+ let target = resource;
189
+ if (typeof resource === "string") {
190
+ target = await window.sibStore.getData(resource, CLIENT_CONTEXT);
191
+ }
192
+ if (typeof resource !== "string" && resource.isFullResource) {
193
+ if (!resource.isFullResource?.()) {
194
+ target = await window.sibStore.getData(
195
+ resource["@id"],
196
+ CLIENT_CONTEXT
197
+ );
198
+ }
199
+ }
200
+ if (typeof resource !== "string" && !resource.isFullResource) {
201
+ // Edge case when calling getProxyValue with an already
202
+ // fetched resource with server search, not a proxy
203
+ (target as Resource).properties = Object.keys(target);
204
+ (target as Resource).get = (property: any) =>
205
+ (target as Resource)[property];
206
+ }
207
+ if (!target) return { _originalResource: target };
208
+ if (typeof target === "object" && target !== null) {
209
+ if (target["ldp:contains"]) {
210
+ const value = this._normalizeLdpContains(target["ldp:contains"]);
211
+ return await this._expandContainer(value, recursive);
212
+ }
213
+ return await this._getProperties(target, recursive, targetProperties);
214
+ }
215
+ }
216
+ return;
217
+ } catch (e) {
218
+ if (import.meta.env.DEV) console.error(e);
219
+ }
220
+ }
221
+
222
+ async _responseAdaptator(response: Resource) {
223
+ return Promise.resolve(response);
224
+ }
225
+
226
+ gatekeeper() {
227
+ if (
228
+ !this.orbit ||
229
+ (!this.noRouter &&
230
+ this.route &&
231
+ this.currentRoute &&
232
+ !this.route.startsWith(this.currentRoute))
233
+ ) {
234
+ return nothing;
235
+ }
236
+
237
+ if (!this.dataSrc) {
238
+ return nothing;
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,37 @@
1
+ /*
2
+ Common code for components
3
+ Handle cache invalidation based on keywords
4
+ */
5
+ const setupCacheInvalidation = (
6
+ component: any,
7
+ { keywords = [] as string[], attributes = ["dataSrc"] } = {}
8
+ ) => {
9
+ if (keywords && attributes) {
10
+ if (component.caching === undefined) {
11
+ component.caching = 0;
12
+ }
13
+ if (component.hasCachedDatas === undefined) {
14
+ component.hasCachedDatas = false;
15
+ }
16
+
17
+ component.cacheListener = (e: Event) => {
18
+ const resource = e.detail.id || e.detail.resource["@id"];
19
+ if (keywords.some((keyword) => resource?.includes(keyword))) {
20
+ for (const attribute of attributes) {
21
+ if (component[attribute] && resource !== component[attribute]) {
22
+ window.sibStore.clearCache(component[attribute]);
23
+ }
24
+ }
25
+ component.caching++;
26
+ component.hasCachedDatas = false;
27
+ component.requestUpdate();
28
+ }
29
+ };
30
+
31
+ component._subscriptions.add(["save", component.cacheListener]);
32
+
33
+ component._subscribe();
34
+ }
35
+ };
36
+
37
+ export default setupCacheInvalidation;
@@ -0,0 +1,32 @@
1
+ /*
2
+ Common code for components
3
+ Handle cache invalidation based on keywords
4
+ */
5
+ const setupCacheOnResourceReady = (component: any, { keywords = [] } = {}) => {
6
+ if (keywords) {
7
+ if (component.caching === undefined) {
8
+ component.caching = 0;
9
+ }
10
+ if (component.hasCachedDatas === undefined) {
11
+ component.hasCachedDatas = false;
12
+ }
13
+
14
+ component.resourceCacheListener = (e: Event) => {
15
+ const resource = e.detail.id || e.detail.resource["@id"];
16
+ if (keywords.some((keyword) => resource?.includes(keyword))) {
17
+ component.caching++;
18
+ component.hasCachedDatas = false;
19
+ component.requestUpdate();
20
+ }
21
+ };
22
+
23
+ component._subscriptions.add([
24
+ "resourceReady",
25
+ component.resourceCacheListener,
26
+ ]);
27
+
28
+ component._subscribe();
29
+ }
30
+ };
31
+
32
+ export default setupCacheOnResourceReady;
@@ -0,0 +1,73 @@
1
+ import uniq from "@helpers/utils/uniq";
2
+ /*
3
+ Common code for components
4
+ Handles uniq, route, optional orbit interface, subscriptions manager for each component
5
+ */
6
+ const setupComponentSubscriptions = ({
7
+ component,
8
+ defaultRoute = false,
9
+ ignoreRouter = false,
10
+ }: {
11
+ component: any;
12
+ defaultRoute: boolean;
13
+ ignoreRouter: boolean;
14
+ }) => {
15
+ if (!component.uniq) {
16
+ component.uniq = uniq();
17
+ if (defaultRoute && !component.route && !ignoreRouter) {
18
+ component.route = defaultRoute;
19
+ }
20
+ }
21
+ component._subscriptions = new Set();
22
+ if (!ignoreRouter) {
23
+ if (!component.route) {
24
+ component.route = component.uniq;
25
+ if (window.orbit) {
26
+ component.route = window.orbit.getRoute(component.uniq);
27
+ }
28
+ }
29
+ component.noRouter = true;
30
+ let router = document.querySelector("solid-router");
31
+ while (router) {
32
+ component.noRouter = false;
33
+ component.currentRoute = router.currentRouteName;
34
+ component.currentResource = window.sibRouter.currentResource;
35
+ router = document.querySelector(
36
+ `[data-view="${router.currentRouteName}"] solid-router`,
37
+ );
38
+ }
39
+
40
+ component.navigationListener = () => {
41
+ // component.currentRoute = e.detail?.route;
42
+ // component.currentResource = window.sibRouter.currentResource;
43
+ let router = document.querySelector("solid-router");
44
+ while (router) {
45
+ component.noRouter = false;
46
+ component.currentRoute = router.currentRouteName;
47
+ component.currentResource = window.sibRouter.currentResource;
48
+ router = document.querySelector(
49
+ `[data-view="${router.currentRouteName}"] solid-router`,
50
+ );
51
+ }
52
+ component.requestUpdate();
53
+ };
54
+ component._subscriptions.add(["navigate", component.navigationListener]);
55
+ }
56
+
57
+ component._subscribe = () => {
58
+ component._unsubscribe();
59
+ for (const subscription of component._subscriptions) {
60
+ document.addEventListener(subscription[0], subscription[1]);
61
+ }
62
+ };
63
+
64
+ component._unsubscribe = () => {
65
+ for (const subscription of component._subscriptions) {
66
+ document.removeEventListener(subscription[0], subscription[1]);
67
+ }
68
+ };
69
+
70
+ component._subscribe();
71
+ };
72
+
73
+ export default setupComponentSubscriptions;
@@ -0,0 +1,20 @@
1
+ /*
2
+ Common code for components
3
+ Handle cache invalidation based on keywords
4
+ */
5
+ const setupOnSaveReset = (component: any, { keywords = [] } = {}) => {
6
+ if (keywords) {
7
+ component.saveListener = (e: Event) => {
8
+ const resource = e.detail.id || e.detail.resource["@id"];
9
+ if (keywords.some((keyword) => resource?.includes(keyword))) {
10
+ component._setValue({ target: { value: "" } });
11
+ }
12
+ };
13
+
14
+ component._subscriptions.add(["save", component.saveListener]);
15
+
16
+ component._subscribe();
17
+ }
18
+ };
19
+
20
+ export default setupOnSaveReset;
@@ -0,0 +1,43 @@
1
+ import type { Resource } from "@src/component";
2
+
3
+ export const recusiveRemovePath = (obj: Resource, pathArray: string[]) => {
4
+ if (!obj || pathArray.length === 0) return;
5
+ const key = pathArray.shift();
6
+
7
+ if (key !== undefined) {
8
+ if (pathArray.length === 0) {
9
+ delete obj[key];
10
+ } else if (obj[key] && typeof obj[key] === "object") {
11
+ recusiveRemovePath(obj[key], pathArray);
12
+ }
13
+ }
14
+ };
15
+
16
+ export const dataBuilder = (
17
+ resource: Resource,
18
+ pathToRemove: string[] = [],
19
+ replacements: object = {},
20
+ removeThenReplace = false,
21
+ ): Resource => {
22
+ const clone = structuredClone(resource);
23
+
24
+ if (!removeThenReplace) {
25
+ for (const path of pathToRemove) {
26
+ const splittedPath = path.split(".");
27
+ recusiveRemovePath(clone, splittedPath);
28
+ }
29
+ }
30
+
31
+ for (const [key, value] of Object.entries(replacements)) {
32
+ clone[key] = value;
33
+ }
34
+
35
+ if (removeThenReplace) {
36
+ for (const path of pathToRemove) {
37
+ const splittedPath = path.split(".");
38
+ recusiveRemovePath(clone, splittedPath);
39
+ }
40
+ }
41
+
42
+ return clone;
43
+ };
@@ -0,0 +1,29 @@
1
+ const filterGenerator = (objects: { [key: string]: any }[], field: string) => {
2
+ return objects
3
+ .flatMap((obj) => {
4
+ if (obj[field]) {
5
+ if (Array.isArray(obj[field])) {
6
+ return obj[field].map((v) => ({
7
+ label: v?.name || v,
8
+ value: v?.["@id"] || v,
9
+ }));
10
+ }
11
+ return {
12
+ label: obj[field]?.name || obj[field],
13
+ value: obj[field]?.["@id"] || obj[field],
14
+ };
15
+ }
16
+ return;
17
+ })
18
+ .filter(
19
+ (value, index, self) =>
20
+ value?.label &&
21
+ value?.value &&
22
+ index ===
23
+ self.findIndex(
24
+ (t) => t?.label === value?.label && t?.value === value?.value,
25
+ ),
26
+ );
27
+ };
28
+
29
+ export default filterGenerator;
@@ -0,0 +1,80 @@
1
+ import type { Resource } from "@src/component";
2
+
3
+ function isValidDateValue(value: unknown): value is string | number | Date {
4
+ return !Number.isNaN(new Date((value as Date)).getTime());
5
+ }
6
+
7
+ const checkDateIsAfterRecursive = (
8
+ data: any,
9
+ propName: string,
10
+ thresholdDate: Date
11
+ ): boolean => {
12
+ if (data === null || data === undefined) {
13
+ return false;
14
+ }
15
+
16
+ const propPath = propName.split(".");
17
+ let current = data;
18
+
19
+ for (const segment of propPath) {
20
+ if (current && typeof current === "object" && segment in current) {
21
+ current = current[segment];
22
+ } else {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ if (isValidDateValue(current)) {
28
+ const date = new Date(current);
29
+
30
+ if (date > thresholdDate) {
31
+ return true;
32
+ }
33
+ } else {
34
+ return false;
35
+ }
36
+
37
+ if (Array.isArray(data)) {
38
+ return data.some((item) =>
39
+ checkDateIsAfterRecursive(item, propName, thresholdDate)
40
+ );
41
+ }
42
+
43
+ if (typeof data === "object") {
44
+ return Object.entries(data).some(([key, value]) => {
45
+ if (key === propName && isValidDateValue(value)) {
46
+ return false;
47
+ }
48
+ return checkDateIsAfterRecursive(value, propName, thresholdDate);
49
+ });
50
+ }
51
+
52
+ return false;
53
+ };
54
+
55
+ const filterObjectByDateAfter = (
56
+ array: Resource[],
57
+ propName: string,
58
+ thresholdDateString: string
59
+ ): Resource[] => {
60
+ if (
61
+ !propName ||
62
+ !thresholdDateString ||
63
+ typeof thresholdDateString !== "string"
64
+ ) {
65
+ return array;
66
+ }
67
+
68
+ const thresholdDate = new Date(thresholdDateString);
69
+
70
+ if (Number.isNaN(thresholdDate.getTime())) {
71
+ console.warn(`Invalid threshold date provided: ${thresholdDateString}`);
72
+ return array;
73
+ }
74
+
75
+ return array.filter((obj) =>
76
+ checkDateIsAfterRecursive(obj, propName, thresholdDate)
77
+ );
78
+ };
79
+
80
+ export default filterObjectByDateAfter;
@@ -0,0 +1,54 @@
1
+ import type { Resource } from "@src/component";
2
+
3
+ const filterObjectById = (
4
+ array: Resource[],
5
+ propName: string,
6
+ targetId: string
7
+ ): Resource[] => {
8
+ if (!propName || !targetId) {
9
+ return array;
10
+ }
11
+
12
+ return array.filter((obj) => {
13
+ let current = obj;
14
+ const propPath = propName.split(".");
15
+
16
+ for (const segment of propPath) {
17
+ if (current && typeof current === "object" && segment in current) {
18
+ current = current[segment];
19
+ } else {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ const propValue = current;
25
+
26
+ if (!propValue) {
27
+ return false;
28
+ }
29
+
30
+ if (
31
+ typeof propValue === "object" &&
32
+ !Array.isArray(propValue) &&
33
+ propValue !== null &&
34
+ "@id" in propValue &&
35
+ propValue["@id"] === targetId
36
+ ) {
37
+ return true;
38
+ }
39
+
40
+ if (Array.isArray(propValue)) {
41
+ return propValue.some(
42
+ (item) =>
43
+ typeof item === "object" &&
44
+ item !== null &&
45
+ "@id" in item &&
46
+ item["@id"] === targetId
47
+ );
48
+ }
49
+
50
+ return false;
51
+ });
52
+ };
53
+
54
+ export default filterObjectById;