@retailcrm/embed-ui 0.1.0 → 0.2.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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "types": [
3
+ {"type": "feat", "section": "Features"},
4
+ {"type": "fix", "section": "Fixes"},
5
+ {"type": "chore", "hidden": true},
6
+ {"type": "docs", "hidden": true},
7
+ {"type": "style", "hidden": true},
8
+ {"type": "refactor", "hidden": true},
9
+ {"type": "perf", "hidden": true},
10
+ {"type": "test", "hidden": true}
11
+ ]
12
+ }
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [0.2.0](https://github.com/retailcrm/embed-ui/compare/v0.1.0...v0.2.0) (2024-11-02)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * Removed all types from types/endpoint.d.ts, old ones were replaced with types from types/widget.d.ts
11
+ * Removed previous pages API, which was replaced with reactive context API
12
+
13
+ ### Features
14
+
15
+ * Scaffolding logic for widgets, reactive context, json metadata for creating documentation ([9ddd8f8](https://github.com/retailcrm/embed-ui/commit/9ddd8f89759fbeb964556a1401c8f23af6c51467))
16
+
5
17
  ## [0.1.0](https://github.com/retailcrm/embed-ui/compare/v0.0.1...v0.1.0) (2024-04-27)
6
18
 
7
19
 
package/Makefile ADDED
@@ -0,0 +1,48 @@
1
+ TARGET_HEADER=@echo -e '===== \e[34m' $@ '\e[0m'
2
+ YARN=docker-compose run --rm node yarn
3
+
4
+ .PHONY: node_modules
5
+ node_modules: package.json yarn.lock ## Installs dependencies
6
+ $(TARGET_HEADER)
7
+ @docker-compose run --rm node yarn install --silent
8
+ @touch node_modules || true
9
+
10
+ .PHONY: build
11
+ build: ## Builds the package
12
+ $(TARGET_HEADER)
13
+ $(YARN) build
14
+ $(YARN) generate:meta
15
+
16
+ .PHONY: release
17
+ release: ## Bumps version and creates tag
18
+ $(TARGET_HEADER)
19
+ ifdef as
20
+ $(YARN) release:$(as)
21
+ else
22
+ $(YARN) release
23
+ endif
24
+
25
+ .PHONY: tests
26
+ tests: ## Runs autotests
27
+ $(TARGET_HEADER)
28
+ ifdef cli
29
+ $(YARN) test $(cli) --passWithNoTests
30
+ else
31
+ $(YARN) test
32
+ endif
33
+
34
+ .PHONY: help
35
+ help: ## Calls recipes list
36
+ @cat $(MAKEFILE_LIST) | grep -e "^[a-zA-Z_\-]*: *.*## *" | awk '\
37
+ BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
38
+
39
+ # Colors
40
+ $(call computable,CC_BLACK,$(shell tput -Txterm setaf 0 2>/dev/null))
41
+ $(call computable,CC_RED,$(shell tput -Txterm setaf 1 2>/dev/null))
42
+ $(call computable,CC_GREEN,$(shell tput -Txterm setaf 2 2>/dev/null))
43
+ $(call computable,CC_YELLOW,$(shell tput -Txterm setaf 3 2>/dev/null))
44
+ $(call computable,CC_BLUE,$(shell tput -Txterm setaf 4 2>/dev/null))
45
+ $(call computable,CC_MAGENTA,$(shell tput -Txterm setaf 5 2>/dev/null))
46
+ $(call computable,CC_CYAN,$(shell tput -Txterm setaf 6 2>/dev/null))
47
+ $(call computable,CC_WHITE,$(shell tput -Txterm setaf 7 2>/dev/null))
48
+ $(call computable,CC_END,$(shell tput -Txterm sgr0 2>/dev/null))
package/dist/index.cjs ADDED
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const rpc = require("@remote-ui/rpc");
4
+ const pinia = require("pinia");
5
+ const remote = require("@omnicajs/vue-remote/remote");
6
+ const vue = require("vue");
7
+ const keysOf = (o) => Object.keys(o);
8
+ const injectAccessor = (endpoint) => {
9
+ return (context) => {
10
+ context.store.endpoint = endpoint;
11
+ };
12
+ };
13
+ const defineContext = (id, schema2) => {
14
+ return pinia.defineStore(id, {
15
+ state() {
16
+ return {
17
+ ...keysOf(schema2).reduce((state, field) => ({
18
+ ...state,
19
+ [field]: schema2[field].defaults()
20
+ }), {})
21
+ };
22
+ },
23
+ getters: {
24
+ schema: () => schema2
25
+ },
26
+ actions: {
27
+ async initialize() {
28
+ const context = this;
29
+ const endpoint = this.endpoint;
30
+ const state = await endpoint.call.get(id, "~");
31
+ keysOf(schema2).forEach((field) => {
32
+ context[field] = state[field];
33
+ endpoint.call.on(id, `change:${String(field)}`, (value) => {
34
+ context[field] = value;
35
+ });
36
+ });
37
+ },
38
+ set(field, value) {
39
+ if (!(field in schema2)) {
40
+ throw new Error(`[crm:embed:remote] Field ${String(field)} is not present in context ${id}`);
41
+ }
42
+ if (schema2[field].readonly) {
43
+ throw new Error(`[crm:embed:remote] Field ${String(field)} is readonly in context ${id}`);
44
+ }
45
+ if (!schema2[field].accepts(value)) {
46
+ throw new Error(`[crm:embed:remote] Invalid value for field ${String(field)} in context ${id}`);
47
+ }
48
+ const context = this;
49
+ const endpoint = this.endpoint;
50
+ context[field] = value;
51
+ endpoint.call.set(id, field, value);
52
+ }
53
+ }
54
+ });
55
+ };
56
+ const useField = (store, field) => {
57
+ if (store.schema[field].readonly) {
58
+ return vue.computed(() => store[field]);
59
+ }
60
+ return vue.computed({
61
+ get: () => store[field],
62
+ set: (value) => {
63
+ store.set(field, value);
64
+ }
65
+ });
66
+ };
67
+ const withMeta = (predicate, type) => {
68
+ return Object.assign(predicate, { type });
69
+ };
70
+ const isNull = withMeta((value) => value === null, "null");
71
+ const isNumber = withMeta((value) => typeof value === "number", "number");
72
+ const isString = withMeta((value) => typeof value === "string", "string");
73
+ const arrayOf = (predicate) => withMeta(
74
+ (value) => {
75
+ return Array.isArray(value) && value.every(predicate);
76
+ },
77
+ `Array<${predicate.type}>`
78
+ );
79
+ const oneOf = (...predicates) => withMeta(
80
+ (value) => {
81
+ return predicates.some((predicate) => predicate(value));
82
+ },
83
+ predicates.map((p) => p.type).join(" | ")
84
+ );
85
+ const schema$3 = {
86
+ "id": {
87
+ accepts: oneOf(isNumber, isNull),
88
+ defaults: () => null,
89
+ readonly: true
90
+ },
91
+ "externalId": {
92
+ accepts: isString,
93
+ defaults: () => null,
94
+ readonly: true
95
+ },
96
+ "email": {
97
+ accepts: isString,
98
+ defaults: () => "",
99
+ readonly: true
100
+ },
101
+ "phones": {
102
+ accepts: arrayOf(isString),
103
+ defaults: () => [],
104
+ readonly: true
105
+ }
106
+ };
107
+ const useContext$3 = defineContext("customer/card", schema$3);
108
+ const schema$2 = {
109
+ value: {
110
+ accepts: isString,
111
+ defaults: () => "",
112
+ readonly: true
113
+ },
114
+ index: {
115
+ accepts: isNumber,
116
+ defaults: () => 0,
117
+ readonly: true
118
+ }
119
+ };
120
+ const useContext$2 = defineContext("customer/card:phone", schema$2);
121
+ const schema$1 = {
122
+ "customer.email": {
123
+ accepts: oneOf(isString, isNull),
124
+ defaults: () => null,
125
+ readonly: false
126
+ },
127
+ "customer.phone": {
128
+ accepts: oneOf(isString, isNull),
129
+ defaults: () => null,
130
+ readonly: false
131
+ },
132
+ "delivery.address": {
133
+ accepts: oneOf(isString, isNull),
134
+ defaults: () => null,
135
+ readonly: false
136
+ }
137
+ };
138
+ const useContext$1 = defineContext("order/card", schema$1);
139
+ const locales = ["en-GB", "es-ES", "ru-RU"];
140
+ const isLocale = withMeta(
141
+ (value) => locales.includes(value),
142
+ locales.map((l) => `'${l}'`).join(" | ")
143
+ );
144
+ const schema = {
145
+ "order.templates.number.api": {
146
+ accepts: isString,
147
+ defaults: () => "",
148
+ readonly: true
149
+ },
150
+ "order.templates.number.crm": {
151
+ accepts: isString,
152
+ defaults: () => "",
153
+ readonly: true
154
+ },
155
+ "system.locale": {
156
+ accepts: isLocale,
157
+ defaults: () => "en-GB",
158
+ readonly: true
159
+ }
160
+ };
161
+ const useContext = defineContext("settings", schema);
162
+ const createRoot = async (channel) => {
163
+ const root = remote.createRemoteRoot(channel, {
164
+ components: [
165
+ "UiButton",
166
+ "UiError",
167
+ "UiLink",
168
+ "UiLoader",
169
+ "UiMenuItem",
170
+ "UiMenuItemGroup",
171
+ "UiModalSidebar",
172
+ "UiModalWindow",
173
+ "UiModalWindowSurface",
174
+ "UiScrollbar",
175
+ "UiTag",
176
+ "UiTransition"
177
+ ]
178
+ });
179
+ await root.mount();
180
+ return root;
181
+ };
182
+ const createWidgetEndpoint = (widget, messenger) => {
183
+ const endpoint = rpc.createEndpoint(messenger);
184
+ const pinia$1 = pinia.createPinia();
185
+ pinia$1.use(injectAccessor(endpoint));
186
+ let onRelease = () => {
187
+ };
188
+ endpoint.expose({
189
+ async run(channel, target) {
190
+ rpc.retain(channel);
191
+ const root = await createRoot(channel);
192
+ await root.mount();
193
+ const { createApp } = remote.createRemoteRenderer(root);
194
+ const destroy = await widget.run(createApp, root, pinia$1, target);
195
+ onRelease = () => {
196
+ destroy();
197
+ rpc.release(channel);
198
+ };
199
+ },
200
+ release() {
201
+ onRelease();
202
+ onRelease = () => {
203
+ };
204
+ }
205
+ });
206
+ return endpoint;
207
+ };
208
+ exports.createWidgetEndpoint = createWidgetEndpoint;
209
+ exports.customerCardPhoneSchema = schema$2;
210
+ exports.customerCardSchema = schema$3;
211
+ exports.orderCardSchema = schema$1;
212
+ exports.settingsSchema = schema;
213
+ exports.useCustomerCardContext = useContext$3;
214
+ exports.useCustomerCardPhoneContext = useContext$2;
215
+ exports.useField = useField;
216
+ exports.useOrderCardContext = useContext$1;
217
+ exports.useSettingsContext = useContext;
@@ -0,0 +1,75 @@
1
+ import { ComputedRef } from 'vue';
2
+ import { Context } from '../../../types/context/schema';
3
+ import { Context as Context_2 } from '../../types/context/schema';
4
+ import { ContextAccessor } from '../types/context/schema';
5
+ import { ContextSchema } from '../../types/context/schema';
6
+ import { Endpoint } from '@remote-ui/rpc';
7
+ import { IsReadonly } from '../../types/context/schema';
8
+ import { MessageEndpoint } from '@remote-ui/rpc';
9
+ import { Schema } from '../../../types/context/customer/card';
10
+ import { Schema as Schema_2 } from '../../../types/context/customer/card-phone';
11
+ import { Schema as Schema_3 } from '../../../types/context/order/card';
12
+ import { Schema as Schema_4 } from '../../types/context/settings';
13
+ import { SchemaList } from '../types/context';
14
+ import { Store } from 'pinia';
15
+ import { StoreDefinition } from 'pinia';
16
+ import { TypeOf } from '../../../types/context/schema';
17
+ import { TypeOf as TypeOf_2 } from '../../types/context/schema';
18
+ import { WidgetRunner } from '../types/widget';
19
+ import { WritableComputedRef } from 'vue';
20
+
21
+ declare type Computed<S extends ContextSchema, F extends keyof S> = IsReadonly<S[F]> extends true ? ComputedRef<TypeOf_2<S[F]>> : WritableComputedRef<TypeOf_2<S[F]>>;
22
+
23
+ export declare const createWidgetEndpoint: (widget: WidgetRunner, messenger: MessageEndpoint) => Endpoint<ContextAccessor<SchemaList>>;
24
+
25
+ export declare const customerCardPhoneSchema: Schema_2;
26
+
27
+ export declare const customerCardSchema: Schema;
28
+
29
+ export declare const orderCardSchema: Schema_3;
30
+
31
+ export declare const settingsSchema: Schema_4;
32
+
33
+ export declare const useCustomerCardContext: StoreDefinition<"customer/card", Context<Schema>, {
34
+ schema: () => Schema;
35
+ }, {
36
+ initialize(): Promise<void>;
37
+ set<F extends keyof Schema>(field: F, value: TypeOf<Schema[F]>): void;
38
+ }>;
39
+
40
+ export declare const useCustomerCardPhoneContext: StoreDefinition<"customer/card:phone", Context<Schema_2>, {
41
+ schema: () => Schema_2;
42
+ }, {
43
+ initialize(): Promise<void>;
44
+ set<F extends keyof Schema_2>(field: F, value: TypeOf<Schema_2[F]>): void;
45
+ }>;
46
+
47
+ export declare const useField: <S extends ContextSchema, F extends keyof S>(store: Store<string, Context_2<S>, {
48
+ schema(): S;
49
+ }, {
50
+ initialize(): Promise<void>;
51
+ set<F_1 extends keyof S>(field: F_1, value: TypeOf_2<S[F_1]>): void;
52
+ }>, field: F) => Computed<S, F>;
53
+
54
+ export declare const useOrderCardContext: StoreDefinition<"order/card", Context<Schema_3>, {
55
+ schema: () => Schema_3;
56
+ }, {
57
+ initialize(): Promise<void>;
58
+ set<F extends keyof Schema_3>(field: F, value: TypeOf<Schema_3[F]>): void;
59
+ }>;
60
+
61
+ export declare const useSettingsContext: StoreDefinition<"settings", Context_2<Schema_4>, {
62
+ schema: () => Schema_4;
63
+ }, {
64
+ initialize(): Promise<void>;
65
+ set<F extends keyof Schema_4>(field: F, value: TypeOf_2<Schema_4[F]>): void;
66
+ }>;
67
+
68
+ export { }
69
+
70
+
71
+ declare module 'pinia' {
72
+ interface PiniaCustomProperties {
73
+ endpoint: Endpoint<ContextAccessor>;
74
+ }
75
+ }
package/dist/index.mjs ADDED
@@ -0,0 +1,217 @@
1
+ import { createEndpoint, retain, release } from "@remote-ui/rpc";
2
+ import { defineStore, createPinia } from "pinia";
3
+ import { createRemoteRenderer, createRemoteRoot } from "@omnicajs/vue-remote/remote";
4
+ import { computed } from "vue";
5
+ const keysOf = (o) => Object.keys(o);
6
+ const injectAccessor = (endpoint) => {
7
+ return (context) => {
8
+ context.store.endpoint = endpoint;
9
+ };
10
+ };
11
+ const defineContext = (id, schema2) => {
12
+ return defineStore(id, {
13
+ state() {
14
+ return {
15
+ ...keysOf(schema2).reduce((state, field) => ({
16
+ ...state,
17
+ [field]: schema2[field].defaults()
18
+ }), {})
19
+ };
20
+ },
21
+ getters: {
22
+ schema: () => schema2
23
+ },
24
+ actions: {
25
+ async initialize() {
26
+ const context = this;
27
+ const endpoint = this.endpoint;
28
+ const state = await endpoint.call.get(id, "~");
29
+ keysOf(schema2).forEach((field) => {
30
+ context[field] = state[field];
31
+ endpoint.call.on(id, `change:${String(field)}`, (value) => {
32
+ context[field] = value;
33
+ });
34
+ });
35
+ },
36
+ set(field, value) {
37
+ if (!(field in schema2)) {
38
+ throw new Error(`[crm:embed:remote] Field ${String(field)} is not present in context ${id}`);
39
+ }
40
+ if (schema2[field].readonly) {
41
+ throw new Error(`[crm:embed:remote] Field ${String(field)} is readonly in context ${id}`);
42
+ }
43
+ if (!schema2[field].accepts(value)) {
44
+ throw new Error(`[crm:embed:remote] Invalid value for field ${String(field)} in context ${id}`);
45
+ }
46
+ const context = this;
47
+ const endpoint = this.endpoint;
48
+ context[field] = value;
49
+ endpoint.call.set(id, field, value);
50
+ }
51
+ }
52
+ });
53
+ };
54
+ const useField = (store, field) => {
55
+ if (store.schema[field].readonly) {
56
+ return computed(() => store[field]);
57
+ }
58
+ return computed({
59
+ get: () => store[field],
60
+ set: (value) => {
61
+ store.set(field, value);
62
+ }
63
+ });
64
+ };
65
+ const withMeta = (predicate, type) => {
66
+ return Object.assign(predicate, { type });
67
+ };
68
+ const isNull = withMeta((value) => value === null, "null");
69
+ const isNumber = withMeta((value) => typeof value === "number", "number");
70
+ const isString = withMeta((value) => typeof value === "string", "string");
71
+ const arrayOf = (predicate) => withMeta(
72
+ (value) => {
73
+ return Array.isArray(value) && value.every(predicate);
74
+ },
75
+ `Array<${predicate.type}>`
76
+ );
77
+ const oneOf = (...predicates) => withMeta(
78
+ (value) => {
79
+ return predicates.some((predicate) => predicate(value));
80
+ },
81
+ predicates.map((p) => p.type).join(" | ")
82
+ );
83
+ const schema$3 = {
84
+ "id": {
85
+ accepts: oneOf(isNumber, isNull),
86
+ defaults: () => null,
87
+ readonly: true
88
+ },
89
+ "externalId": {
90
+ accepts: isString,
91
+ defaults: () => null,
92
+ readonly: true
93
+ },
94
+ "email": {
95
+ accepts: isString,
96
+ defaults: () => "",
97
+ readonly: true
98
+ },
99
+ "phones": {
100
+ accepts: arrayOf(isString),
101
+ defaults: () => [],
102
+ readonly: true
103
+ }
104
+ };
105
+ const useContext$3 = defineContext("customer/card", schema$3);
106
+ const schema$2 = {
107
+ value: {
108
+ accepts: isString,
109
+ defaults: () => "",
110
+ readonly: true
111
+ },
112
+ index: {
113
+ accepts: isNumber,
114
+ defaults: () => 0,
115
+ readonly: true
116
+ }
117
+ };
118
+ const useContext$2 = defineContext("customer/card:phone", schema$2);
119
+ const schema$1 = {
120
+ "customer.email": {
121
+ accepts: oneOf(isString, isNull),
122
+ defaults: () => null,
123
+ readonly: false
124
+ },
125
+ "customer.phone": {
126
+ accepts: oneOf(isString, isNull),
127
+ defaults: () => null,
128
+ readonly: false
129
+ },
130
+ "delivery.address": {
131
+ accepts: oneOf(isString, isNull),
132
+ defaults: () => null,
133
+ readonly: false
134
+ }
135
+ };
136
+ const useContext$1 = defineContext("order/card", schema$1);
137
+ const locales = ["en-GB", "es-ES", "ru-RU"];
138
+ const isLocale = withMeta(
139
+ (value) => locales.includes(value),
140
+ locales.map((l) => `'${l}'`).join(" | ")
141
+ );
142
+ const schema = {
143
+ "order.templates.number.api": {
144
+ accepts: isString,
145
+ defaults: () => "",
146
+ readonly: true
147
+ },
148
+ "order.templates.number.crm": {
149
+ accepts: isString,
150
+ defaults: () => "",
151
+ readonly: true
152
+ },
153
+ "system.locale": {
154
+ accepts: isLocale,
155
+ defaults: () => "en-GB",
156
+ readonly: true
157
+ }
158
+ };
159
+ const useContext = defineContext("settings", schema);
160
+ const createRoot = async (channel) => {
161
+ const root = createRemoteRoot(channel, {
162
+ components: [
163
+ "UiButton",
164
+ "UiError",
165
+ "UiLink",
166
+ "UiLoader",
167
+ "UiMenuItem",
168
+ "UiMenuItemGroup",
169
+ "UiModalSidebar",
170
+ "UiModalWindow",
171
+ "UiModalWindowSurface",
172
+ "UiScrollbar",
173
+ "UiTag",
174
+ "UiTransition"
175
+ ]
176
+ });
177
+ await root.mount();
178
+ return root;
179
+ };
180
+ const createWidgetEndpoint = (widget, messenger) => {
181
+ const endpoint = createEndpoint(messenger);
182
+ const pinia = createPinia();
183
+ pinia.use(injectAccessor(endpoint));
184
+ let onRelease = () => {
185
+ };
186
+ endpoint.expose({
187
+ async run(channel, target) {
188
+ retain(channel);
189
+ const root = await createRoot(channel);
190
+ await root.mount();
191
+ const { createApp } = createRemoteRenderer(root);
192
+ const destroy = await widget.run(createApp, root, pinia, target);
193
+ onRelease = () => {
194
+ destroy();
195
+ release(channel);
196
+ };
197
+ },
198
+ release() {
199
+ onRelease();
200
+ onRelease = () => {
201
+ };
202
+ }
203
+ });
204
+ return endpoint;
205
+ };
206
+ export {
207
+ createWidgetEndpoint,
208
+ schema$2 as customerCardPhoneSchema,
209
+ schema$3 as customerCardSchema,
210
+ schema$1 as orderCardSchema,
211
+ schema as settingsSchema,
212
+ useContext$3 as useCustomerCardContext,
213
+ useContext$2 as useCustomerCardPhoneContext,
214
+ useField,
215
+ useContext$1 as useOrderCardContext,
216
+ useContext as useSettingsContext
217
+ };
package/dist/meta.json ADDED
@@ -0,0 +1,246 @@
1
+ {
2
+ "contexts": {
3
+ "customer/card": [
4
+ {
5
+ "name": "id",
6
+ "type": "number | null",
7
+ "description": {
8
+ "en-GB": "Customer ID",
9
+ "es-ES": "",
10
+ "ru-RU": "ID клиента"
11
+ },
12
+ "readonly": true
13
+ },
14
+ {
15
+ "name": "externalId",
16
+ "type": "string",
17
+ "description": {
18
+ "en-GB": "Customer external ID",
19
+ "es-ES": "",
20
+ "ru-RU": "Внешний ID клиента"
21
+ },
22
+ "readonly": true
23
+ },
24
+ {
25
+ "name": "email",
26
+ "type": "string",
27
+ "description": {
28
+ "en-GB": "Customer email",
29
+ "es-ES": "",
30
+ "ru-RU": "Email клиента"
31
+ },
32
+ "readonly": true
33
+ },
34
+ {
35
+ "name": "phones",
36
+ "type": "Array<string>",
37
+ "description": {
38
+ "en-GB": "Customer phone list",
39
+ "es-ES": "",
40
+ "ru-RU": "Список телефонов клиента"
41
+ },
42
+ "readonly": true
43
+ }
44
+ ],
45
+ "customer/card.phone": [
46
+ {
47
+ "name": "value",
48
+ "type": "string",
49
+ "description": {
50
+ "en-GB": "Customer phone",
51
+ "es-ES": "",
52
+ "ru-RU": "Телефон клиента"
53
+ },
54
+ "readonly": true
55
+ },
56
+ {
57
+ "name": "index",
58
+ "type": "number",
59
+ "description": {
60
+ "en-GB": "Serial number of the phone in the list",
61
+ "es-ES": "",
62
+ "ru-RU": "Порядковый номер телефона в списке"
63
+ },
64
+ "readonly": true
65
+ }
66
+ ],
67
+ "order/card": [
68
+ {
69
+ "name": "customer.email",
70
+ "type": "string | null",
71
+ "description": {
72
+ "en-GB": "Customer email",
73
+ "es-ES": "",
74
+ "ru-RU": "Email клиента"
75
+ },
76
+ "readonly": false
77
+ },
78
+ {
79
+ "name": "customer.phone",
80
+ "type": "string | null",
81
+ "description": {
82
+ "en-GB": "Customer phone",
83
+ "es-ES": "",
84
+ "ru-RU": "Телефон клиента"
85
+ },
86
+ "readonly": false
87
+ },
88
+ {
89
+ "name": "delivery.address",
90
+ "type": "string | null",
91
+ "description": {
92
+ "en-GB": "Delivery address",
93
+ "es-ES": "",
94
+ "ru-RU": "Адрес доставки"
95
+ },
96
+ "readonly": false
97
+ }
98
+ ],
99
+ "settings": [
100
+ {
101
+ "name": "order.templates.number.api",
102
+ "type": "string",
103
+ "description": {
104
+ "en-GB": "Number template for orders created with API",
105
+ "es-ES": "",
106
+ "ru-RU": "Шаблон номера заказов, создаваемых через API"
107
+ },
108
+ "readonly": true
109
+ },
110
+ {
111
+ "name": "order.templates.number.crm",
112
+ "type": "string",
113
+ "description": {
114
+ "en-GB": "Number template for orders created with CRM's interface",
115
+ "es-ES": "",
116
+ "ru-RU": "Шаблон номера заказов, создаваемых через интерфейс CRM"
117
+ },
118
+ "readonly": true
119
+ },
120
+ {
121
+ "name": "system.locale",
122
+ "type": "'en-GB' | 'es-ES' | 'ru-RU'",
123
+ "description": {
124
+ "en-GB": "Current system's locale",
125
+ "es-ES": "",
126
+ "ru-RU": "Текущая локаль системы"
127
+ },
128
+ "readonly": true
129
+ }
130
+ ]
131
+ },
132
+ "targets": [
133
+ {
134
+ "id": "customer/card:phone",
135
+ "description": {
136
+ "en-GB": "Widget for customer phone list item",
137
+ "es-ES": "",
138
+ "ru-RU": "Виджет для элемента списка телефонов клиента"
139
+ },
140
+ "location": {
141
+ "en-GB": "Right after the phone number in the list",
142
+ "es-ES": "",
143
+ "ru-RU": "Сразу после номера телефона в списке"
144
+ },
145
+ "contexts": [
146
+ "customer/card",
147
+ "settings"
148
+ ]
149
+ },
150
+ {
151
+ "id": "order/card:customer.after",
152
+ "description": {
153
+ "en-GB": "Widget for section with customer data",
154
+ "es-ES": "",
155
+ "ru-RU": "Виджет для секции с данными клиента"
156
+ },
157
+ "location": {
158
+ "en-GB": "Right under the input fields",
159
+ "es-ES": "",
160
+ "ru-RU": "Сразу под полями ввода"
161
+ },
162
+ "contexts": [
163
+ "order/card",
164
+ "settings"
165
+ ]
166
+ },
167
+ {
168
+ "id": "order/card:customer.email",
169
+ "description": {
170
+ "en-GB": "Widget for customer email input field",
171
+ "es-ES": "",
172
+ "ru-RU": "Виджет для поля ввода email клиента"
173
+ },
174
+ "location": {
175
+ "en-GB": "Right after the input field",
176
+ "es-ES": "",
177
+ "ru-RU": "Сразу после поля ввода"
178
+ },
179
+ "contexts": [
180
+ "order/card",
181
+ "settings"
182
+ ]
183
+ },
184
+ {
185
+ "id": "order/card:customer.phone",
186
+ "description": {
187
+ "en-GB": "Widget for customer phone input field",
188
+ "es-ES": "",
189
+ "ru-RU": "Виджет для поля ввода телефона клиента"
190
+ },
191
+ "location": {
192
+ "en-GB": "Right after the input field",
193
+ "es-ES": "",
194
+ "ru-RU": "Сразу после поля ввода"
195
+ },
196
+ "contexts": [
197
+ "order/card",
198
+ "settings"
199
+ ]
200
+ },
201
+ {
202
+ "id": "order/card:delivery.address",
203
+ "description": {
204
+ "en-GB": "Widget for delivery address input field",
205
+ "es-ES": "",
206
+ "ru-RU": "Виджет для поля ввода адреса доставки"
207
+ },
208
+ "location": {
209
+ "en-GB": "Right under the input field",
210
+ "es-ES": "",
211
+ "ru-RU": "Под полем ввода адреса"
212
+ },
213
+ "contexts": [
214
+ "order/card",
215
+ "settings"
216
+ ]
217
+ }
218
+ ],
219
+ "pages": [
220
+ {
221
+ "id": "customer/card",
222
+ "description": {
223
+ "en-GB": "Customer page",
224
+ "es-ES": "",
225
+ "ru-RU": "Страница клиента"
226
+ },
227
+ "targets": [
228
+ "customer/card:phone"
229
+ ]
230
+ },
231
+ {
232
+ "id": "order/card",
233
+ "description": {
234
+ "en-GB": "Page with the order creation/editing form",
235
+ "es-ES": "",
236
+ "ru-RU": "Страница с формой создания/редактирования заказа"
237
+ },
238
+ "targets": [
239
+ "order/card:customer.after",
240
+ "order/card:customer.email",
241
+ "order/card:customer.phone",
242
+ "order/card:delivery.address"
243
+ ]
244
+ }
245
+ ]
246
+ }
package/package.json CHANGED
@@ -1,39 +1,64 @@
1
1
  {
2
2
  "name": "@retailcrm/embed-ui",
3
3
  "type": "module",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "API and components for creating RetailCRM UI extensions",
6
- "main": "index.js",
7
6
  "repository": "git@github.com:retailcrm/embed-ui.git",
8
7
  "author": "RetailDriverLLC <integration@retailcrm.ru>",
9
8
  "license": "MIT",
10
9
  "contributors": [
11
10
  "Kirill Zaytsev <zaytsev.cmath10@gmail.com>"
12
11
  ],
12
+ "exports": {
13
+ ".": {
14
+ "import": "./dist/index.mjs",
15
+ "require": "./dist/index.cjs",
16
+ "types": "./dist/index.d.ts",
17
+ "default": "./dist/index.mjs"
18
+ },
19
+ "./dist/*": "./dist/*",
20
+ "./types/*": "./types/*"
21
+ },
13
22
  "scripts": {
14
- "lint": "eslint .",
15
- "release": "standard-version",
16
- "release:major": "standard-version --release-as major",
17
- "release:minor": "standard-version --release-as minor",
18
- "release:patch": "standard-version --release-as patch"
23
+ "build": "vite build",
24
+ "eslint": "eslint .",
25
+ "generate:meta": "yes | npx tsx scripts/generate-meta.ts",
26
+ "release": "yes | npx standard-version",
27
+ "release:major": "yes | npx standard-version --release-as major",
28
+ "release:minor": "yes | npx standard-version --release-as minor",
29
+ "release:patch": "yes | npx standard-version --release-as patch",
30
+ "test": "vitest --run"
19
31
  },
20
32
  "dependencies": {
21
- "@omnicajs/vue-remote": "^0.1.3",
33
+ "@omnicajs/vue-remote": "^0.2.0",
22
34
  "@remote-ui/rpc": "^1.4.5"
23
35
  },
36
+ "peerDependencies": {
37
+ "pinia": "^2.2",
38
+ "vue": "^3.5"
39
+ },
24
40
  "devDependencies": {
25
41
  "@eslint/eslintrc": "^3.0.2",
26
- "@eslint/js": "^9.1.1",
42
+ "@eslint/js": "^9.13.0",
27
43
  "@stylistic/eslint-plugin": "^1.7.2",
28
- "eslint": "^9.1.1",
29
- "eslint-config-standard-with-typescript": "^43.0.1",
30
- "eslint-plugin-import": "^2.25.2",
31
- "eslint-plugin-n": "^17.3.1",
32
- "eslint-plugin-promise": "^6.1.1",
33
- "globals": "^15.0.0",
34
- "standard-version": "^9.5.0",
44
+ "@types/node": "^22.7.9",
45
+ "@vitejs/plugin-vue": "^5.1.4",
46
+ "@vue/test-utils": "^2.4.6",
47
+ "eslint": "^9.13.0",
48
+ "eslint-plugin-import": "^2.31.0",
49
+ "eslint-plugin-n": "^17.11.1",
50
+ "eslint-plugin-promise": "^6.6.0",
51
+ "eslint-plugin-vue": "^9.29.1",
52
+ "globals": "^15.11.0",
53
+ "jsdom": "^25.0.1",
54
+ "pinia": "^2.2.4",
55
+ "tsx": "^4.19.2",
35
56
  "typescript": "^5.4.5",
36
- "typescript-eslint": "^7.7.1"
57
+ "typescript-eslint": "^8.11.0",
58
+ "vite": "^5.4.8",
59
+ "vite-plugin-dts": "^4.3.0",
60
+ "vitest": "^2.1.2",
61
+ "vue": "^3.5.12"
37
62
  },
38
63
  "publishConfig": {
39
64
  "access": "public"
@@ -0,0 +1,54 @@
1
+ import type { ContextSchema } from '~types/context/schema'
2
+ import type { SchemaDocumentation } from '~meta'
3
+
4
+ import * as fs from 'node:fs'
5
+
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ import {
9
+ dirname,
10
+ join,
11
+ resolve,
12
+ } from 'node:path'
13
+
14
+ import { keysOf } from '@/utilities'
15
+
16
+ import {
17
+ schemaList,
18
+ schemaListDocumentation,
19
+ targetListDocumentation,
20
+ pageListDocumentation,
21
+ } from '~meta'
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url))
24
+
25
+ const dist = resolve(__dirname, '../dist/')
26
+
27
+ if (!fs.existsSync(dist)) {
28
+ fs.mkdirSync(dist)
29
+ }
30
+
31
+ fs.writeFileSync(join(dist, 'meta.json'), JSON.stringify({
32
+ contexts: keysOf(schemaList).reduce((meta, key) => {
33
+ const schema = schemaList[key] as ContextSchema
34
+
35
+ meta[key] = keysOf(schema).map(field => {
36
+ const accepts = schema[field].accepts
37
+ const documentation = schemaListDocumentation[key] as SchemaDocumentation<ContextSchema>
38
+
39
+ return {
40
+ name: field,
41
+ type: 'type' in accepts ? accepts.type : 'unknown',
42
+ description: documentation[field].description,
43
+ readonly: schema[field].readonly,
44
+ }
45
+ })
46
+
47
+ return meta
48
+ }, {} as Record<string, unknown>),
49
+ targets: keysOf(targetListDocumentation).map(target => ({
50
+ id: target,
51
+ ...targetListDocumentation[target],
52
+ })),
53
+ pages: pageListDocumentation,
54
+ }, null, 2))
@@ -0,0 +1,6 @@
1
+ import type { ReadonlyField } from '../schema'
2
+
3
+ export type Schema = {
4
+ 'value': ReadonlyField<string>;
5
+ 'index': ReadonlyField<number>;
6
+ }
@@ -0,0 +1,8 @@
1
+ import type { ReadonlyField } from '../schema'
2
+
3
+ export type Schema = {
4
+ 'id': ReadonlyField<number | null>;
5
+ 'externalId': ReadonlyField<string | null>;
6
+ 'email': ReadonlyField<string>;
7
+ 'phones': ReadonlyField<string[]>;
8
+ }
@@ -0,0 +1,11 @@
1
+ import type { Schema as CustomerCardSchema } from './customer/card'
2
+ import type { Schema as CustomerCardPhoneSchema } from './customer/card-phone'
3
+ import type { Schema as OrderCardSchema } from './order/card'
4
+ import type { Schema as SettingsSchema } from './settings'
5
+
6
+ export type SchemaList = {
7
+ 'customer/card': CustomerCardSchema;
8
+ 'customer/card.phone': CustomerCardPhoneSchema;
9
+ 'order/card': OrderCardSchema;
10
+ 'settings': SettingsSchema;
11
+ }
@@ -0,0 +1,7 @@
1
+ import type { Field } from '../schema'
2
+
3
+ export type Schema = {
4
+ 'customer.email': Field<string | null>;
5
+ 'customer.phone': Field<string | null>;
6
+ 'delivery.address': Field<string | null>;
7
+ }
@@ -0,0 +1,85 @@
1
+ import type { IsTilda, If } from '../scaffolding'
2
+
3
+ export type Field<Type, Readonly extends boolean = false> = {
4
+ accepts (value: Type): boolean;
5
+ defaults (): Type;
6
+ readonly: Readonly;
7
+ }
8
+
9
+ export type ReadonlyField<Type> = Field<Type, true>
10
+
11
+ export type TypeOf<F> = F extends Field<infer T>
12
+ ? T
13
+ : F extends ReadonlyField<infer T>
14
+ ? T
15
+ : never;
16
+
17
+ export type IsReadonly<F> = F extends Field<unknown, infer R>
18
+ ? R
19
+ : F extends ReadonlyField
20
+ ? true
21
+ : false;
22
+
23
+ export type ContextSchema = {
24
+ [key: string]: Field<unknown, boolean>;
25
+ }
26
+
27
+ export type ContextSchemaMap = {
28
+ [key: string]: ContextSchema;
29
+ }
30
+
31
+ export type Context<S extends ContextSchema> = {
32
+ [F in keyof S]: TypeOf<S[F]>;
33
+ }
34
+
35
+ export type Writable<S extends ContextSchema> = {
36
+ [F in keyof S]: IsReadonly<S[F]> extends true ? never : S[F];
37
+ }
38
+
39
+ export type EventMap<S extends ContextSchema> = {
40
+ [K in keyof S as `change:${K}`]: K;
41
+ }
42
+
43
+ export type EventHandler<
44
+ S extends ContextSchema,
45
+ E extends keyof EventMap<S>
46
+ > = (payload: TypeOf<S[EventMap<S>[E]]>) => void
47
+
48
+ export type ContextAccessor<M extends ContextSchemaMap = ContextSchemaMap> = {
49
+ get <
50
+ C extends keyof M
51
+ >(context: C, field: '~'): Context<M[C]>;
52
+
53
+ get <
54
+ C extends keyof M,
55
+ F extends keyof M[C]
56
+ >(context: C, field: F): Context<M[C]>[F];
57
+
58
+ set <
59
+ C extends keyof M,
60
+ F extends keyof Writable<M[C]>
61
+ >(context: C, field: F, value: Context<M[C]>[F]): void;
62
+
63
+ on<
64
+ C extends keyof M,
65
+ E extends keyof EventMap<M[C]>
66
+ >(context: C, event: E, handler: EventHandler<M[C], E>): void;
67
+ }
68
+
69
+ export type FieldAccessor<S extends ContextSchema> = {
70
+ get <F extends keyof S>(field: F | '~'): If<
71
+ IsTilda<typeof field>,
72
+ Context<S>,
73
+ Context<S>[F]
74
+ >;
75
+
76
+ set <F extends keyof Writable<S>>(
77
+ field: F,
78
+ value: Context<S>[F]
79
+ ): void;
80
+
81
+ on<E extends keyof EventMap<S>>(
82
+ event: E,
83
+ handler: EventHandler<S, E>
84
+ ): void;
85
+ }
@@ -0,0 +1,9 @@
1
+ import type { ReadonlyField } from '~types/context/schema'
2
+
3
+ export type Locale = 'en-GB' | 'es-ES' | 'ru-RU'
4
+
5
+ export type Schema = {
6
+ 'order.templates.number.api': ReadonlyField<string>;
7
+ 'order.templates.number.crm': ReadonlyField<string>;
8
+ 'system.locale': ReadonlyField<Locale>;
9
+ }
@@ -1,2 +1,8 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
1
2
  export type AnyFunction = (...payload: any[]) => unknown
2
3
  export type None = Record<string, never>
4
+
5
+ type IsExact<A, B> = A extends B ? (B extends A ? true : false) : false;
6
+ type IsTilda<A> = IsExact<A, '~'>;
7
+
8
+ type If<Condition, Then, Else> = Condition extends true ? Then : Else;
@@ -0,0 +1,56 @@
1
+ import type {
2
+ CreateAppFunction,
3
+ Component,
4
+ } from 'vue'
5
+
6
+ import type { Channel } from '@omnicajs/vue-remote/dist/remote'
7
+
8
+ import type { Pinia } from 'pinia'
9
+
10
+ import type { RemoteRoot } from '@omnicajs/vue-remote/remote'
11
+
12
+ import type { SchemaList } from './context'
13
+
14
+ export interface WidgetRunner {
15
+ run(
16
+ createApp: CreateAppFunction<
17
+ | Component<RemoteRoot<SchemaOf<string>>>
18
+ | RemoteRoot<SchemaOf<string>>
19
+ >,
20
+ root: RemoteRoot<SchemaOf<string>>,
21
+ pinia: Pinia,
22
+ target: WidgetTarget
23
+ ): Promise<() => void>;
24
+ }
25
+
26
+ export interface WidgetEndpoint {
27
+ run (channel: Channel, target: WidgetTarget): Promise<void>;
28
+ release (): void;
29
+ }
30
+
31
+ export type WidgetTarget = keyof SchemaListByTarget
32
+
33
+ export type SchemaListOf<T extends WidgetTarget> = SchemaListByTarget[T]
34
+ export type SchemaListByTarget = {
35
+ 'customer/card:phone': Pick<SchemaList,
36
+ | 'customer/card'
37
+ | 'customer/card.phone'
38
+ | 'settings'
39
+ >;
40
+ 'order/card:customer.after': Pick<SchemaList,
41
+ | 'order/card'
42
+ | 'settings'
43
+ >;
44
+ 'order/card:customer.email': Pick<SchemaList,
45
+ | 'order/card'
46
+ | 'settings'
47
+ >;
48
+ 'order/card:customer.phone': Pick<SchemaList,
49
+ | 'order/card'
50
+ | 'settings'
51
+ >;
52
+ 'order/card:delivery.address': Pick<SchemaList,
53
+ | 'order/card'
54
+ | 'settings'
55
+ >;
56
+ }
@@ -1,39 +0,0 @@
1
- name: Tests
2
-
3
- on: [push, pull_request]
4
-
5
- concurrency:
6
- group: ${{ github.workflow }}-${{ github.ref }}
7
- cancel-in-progress: true
8
-
9
- jobs:
10
- eslint:
11
- runs-on: ubuntu-latest
12
-
13
- strategy:
14
- matrix:
15
- node-version: [ 18.18.x, 20.x ]
16
-
17
- steps:
18
- - name: Using branch ${{ github.ref }} for repository ${{ github.repository }}.
19
- uses: actions/checkout@v4
20
-
21
- - name: Use Node.js ${{ matrix.node-version }}
22
- uses: actions/setup-node@v4
23
- with:
24
- node-version: ${{ matrix.node-version }}
25
-
26
- - name: Cache dependencies
27
- id: cache-deps
28
- uses: actions/cache@v4
29
- with:
30
- path: .yarn
31
- key: ${{ runner.OS }}-node-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }}
32
- restore-keys: |
33
- ${{ runner.OS }}-node-${{ matrix.node-version }}-yarn-
34
-
35
- - name: Install dependencies
36
- run: yarn install
37
-
38
- - name: Run lint
39
- run: yarn lint
@@ -1,18 +0,0 @@
1
- import type {
2
- AnyFunction,
3
- None,
4
- } from './scaffolding'
5
-
6
- import type { Channel } from '@omnicajs/vue-remote/host'
7
- import type { Endpoint as RemoteEndpoint } from '@remote-ui/rpc'
8
-
9
- export interface EndpointApi<
10
- PageApi extends Record<string, AnyFunction> = None
11
- > {
12
- run (channel: Channel, api: PageApi): Promise<void>;
13
- release (): void;
14
- }
15
-
16
- export type Endpoint<
17
- PageApi extends Record<string, AnyFunction> = None
18
- > = RemoteEndpoint<EndpointApi<PageApi>>
@@ -1,11 +0,0 @@
1
- export type OrderCardMethods = {
2
- getCustomerEmail: () => string | null,
3
- setCustomerEmail: (value: string) => void,
4
-
5
- getCustomerPhone: () => string | null,
6
- setCustomerPhone: (value: string) => void,
7
-
8
- getDeliveryAddress: () => string | null,
9
- setDeliveryAddress: (value: string) => void,
10
- parseDeliveryAddress: () => void,
11
- }