@live-change/frontend-auto-form 0.8.111 → 0.8.113

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.
package/index.js CHANGED
@@ -7,6 +7,9 @@ export { AutoInput, AutoField, AutoEditor }
7
7
  import * as inputConfig from './config.js'
8
8
  export { inputConfig }
9
9
 
10
+ import editorData from './logic/editorData.js'
11
+ export { editorData }
12
+
10
13
  import en from "./locales/en.json"
11
14
  const locales = { en }
12
15
  export { locales }
@@ -1,20 +1,207 @@
1
1
  import { useToast } from 'primevue/usetoast'
2
2
  import { usePath, live, useApi } from '@live-change/vue3-ssr'
3
+ import { ref, computed, inject } from 'vue'
4
+ import { synchronized, defaultData } from '@live-change/vue3-components'
3
5
 
4
- function editorData(options = {}) {
5
- let {
6
+ export default function editorData(options) {
7
+ if(!options) throw new Error('options must be provided')
8
+
9
+ const {
10
+ identifiers,
6
11
  service: serviceName,
7
12
  model: modelName,
8
- draft = false,
9
- methodNames = {}
10
- } = options
11
- if(!service || !model) throw new Error('service and model must be defined')
12
13
 
13
- const path = usePath()
14
- const api = useApi()
15
- const toast = useToast()
14
+ draft = true,
15
+ autoSave = false,
16
+
17
+ updateDataProperty = undefined,
18
+ recursive = true,
19
+ debounce = 600,
20
+ timeField = 'lastUpdate',
21
+
22
+ savedToast = "Saved",
23
+ savedDraftToast = "Draft saved",
24
+ discardedDraftToast = "Draft discarded",
25
+ saveDraftErrorToast = "Error saving draft",
26
+ saveErrorToast = "Error saving",
27
+
28
+ onSaved = () => {},
29
+ onDraftSaved = () => {},
30
+ onDraftDiscarded = () => {},
31
+ onSaveError = () => {},
32
+
33
+ toast = useToast(),
34
+ path = usePath(),
35
+ api = useApi(),
36
+ workingZone = inject('workingZone')
37
+
38
+ } = options
39
+ if(!identifiers) throw new Error('identifiers must be defined')
40
+ if(!serviceName || !modelName) throw new Error('service and model must be defined')
16
41
 
17
42
  const service = api.services[serviceName]
18
43
  const model = service.models[modelName]
44
+ const {
45
+ crudMethods = model.crud,
46
+ identifiersNames = model.identifiers,
47
+ editableProperties = model.editableProperties
48
+ } = options
49
+ if(!crudMethods) throw new Error('crud methods must be defined in model or options')
50
+ if(!identifiers) throw new Error('identifiers names must be defined in model or options')
51
+ if(!editableProperties) throw new Error('editableProperties must be defined in model or options')
52
+
53
+ let idKey = null
54
+ let draftIdParts = []
55
+ for(const identifier of identifiersNames) {
56
+ if(typeof identifier === 'object') {
57
+ for(const [key, value] of Object.entries(identifier)) {
58
+ if(value === 'id') {
59
+ idKey = key
60
+ }
61
+ draftIdParts.push(value)
62
+ }
63
+ } else {
64
+ draftIdParts.push(identifier)
65
+ }
66
+ }
67
+ const draftId = idKey ? identifiers[idKey]
68
+ : identifiersNames.map(identifier => draftIdParts.map(key => JSON.stringify(identifier[key])).join('_'))
69
+
70
+ const draftIdentifiers = {
71
+ actionType: serviceName, action: crudMethods.read, targetType: modelName, target: draftId
72
+ }
73
+
74
+ const savedDataPath = path[serviceName][crudMethods.read](identifiers)
75
+ const draftDataPath = (draft && path.draft.myDraft(draftIdentifiers)) || null
76
+
77
+ const updateAction = api.actions[serviceName][crudMethods.update]
78
+ const createOrUpdateAction = api.actions[serviceName][crudMethods.createOrUpdate]
79
+ const createAction = api.actions[serviceName][crudMethods.create]
80
+ const createOrUpdateDraftAction = draft && api.actions.draft.setOrUpdateMyDraft
81
+ const removeDraftAction = draft && api.actions.draft.resetMyDraft
82
+
83
+ return Promise.all([
84
+ live(savedDataPath), live(draftDataPath)
85
+ ]).then(([savedData, draftData]) => {
86
+ const editableSavedData = computed(() => savedData.value && Object.fromEntries(
87
+ editableProperties.map(prop => [prop, savedData.value[prop]])
88
+ .concat([[timeField, savedData.value[timeField]]])
89
+ ))
90
+ const editableDraftData = computed(() => draft && draftData.value && Object.fromEntries(
91
+ editableProperties.map(prop => [prop, draftData.value.data[prop]])
92
+ .concat([[timeField, draftData.value.data[timeField]]])
93
+ ))
94
+ const source = computed(() => editableDraftData.value || editableSavedData.value || defaultData(model))
95
+
96
+ let savePromise = null
97
+ const saving = ref(false)
98
+ async function saveData(data){
99
+ if(savePromise) await savePromise // wait for previous save
100
+ saving.value = true
101
+ savePromise = (async () => {
102
+ try {
103
+ if(savedData.value) {
104
+ return updateAction(data)
105
+ } else {
106
+ return createAction(data)
107
+ }
108
+ } finally {
109
+ saving.value = false
110
+ savePromise = null
111
+ }
112
+ })()
113
+ if(!autoSave && workingZone) workingZone.addPromise(savePromise.catch(() => {}))
114
+ await savePromise
115
+ }
116
+
117
+ if(draft) {
118
+ function saveDraft(data){
119
+ return createOrUpdateDraftAction({ ...data, from: editableSavedData.value })
120
+ }
121
+ const synchronizedData = synchronized({
122
+ source,
123
+ update: saveDraft,
124
+ updateDataProperty: 'data',
125
+ identifiers: draftIdentifiers,
126
+ recursive,
127
+ autoSave: true,
128
+ debounce,
129
+ timeField,
130
+ resetOnError: false,
131
+ onSave: () => {
132
+ onDraftSaved()
133
+ if(toast && savedDraftToast) toast.add({ severity: 'info', summary: savedDraftToast, life: 1500 })
134
+ },
135
+ onSaveError: (e) => {
136
+ console.error("DRAFT SAVE ERROR", e)
137
+ if(toast && saveDraftErrorToast)
138
+ toast.add({ severity: 'error', summary: saveDraftErrorToast, detail: e.message ?? e, life: 5000 })
139
+ }
140
+ })
141
+
142
+ const changed = computed(() =>
143
+ JSON.stringify(editableSavedData.value) !== JSON.stringify(synchronizedData.value.value))
144
+ const sourceChanged = computed(() =>
145
+ JSON.stringify(draftData.value.from) !== JSON.stringify(editableSavedData.value))
146
+
147
+
148
+ async function save() {
149
+ await saveData()
150
+ if(draftData.value) await removeDraftAction(draftIdentifiers)
151
+ onSaved()
152
+ }
153
+
154
+ async function discardDraft() {
155
+ const discardPromise = removeDraftAction(draftIdentifiers)
156
+ if(workingZone) workingZone.addPromise(discardPromise)
157
+ await discardPromise
158
+ onDraftDiscarded()
159
+ if(toast && discardedDraftToast) toast.add({ severity: 'info', summary: discardedDraftToast, life: 1500 })
160
+ }
161
+
162
+ return {
163
+ value: synchronizedData.value,
164
+ changed,
165
+ save,
166
+ saving,
167
+ discardDraft,
168
+ model,
169
+ resetOnError: false,
170
+ draftChanged: synchronizedData.changed,
171
+ saveDraft: synchronizedData.save,
172
+ savingDraft: synchronizedData.saving,
173
+ sourceChanged /// needed for draft discard on concurrent save
174
+ }
175
+ } else {
176
+ const synchronizedData = synchronized({
177
+ source,
178
+ update: createOrUpdateAction ?? saveData,
179
+ updateDataProperty,
180
+ identifiers,
181
+ recursive,
182
+ autoSave,
183
+ debounce,
184
+ onSave: () => {
185
+ onSaved()
186
+ if(toast && savedDraftToast) toast.add({ severity: 'info', summary: savedDraftToast, life: 1500 })
187
+ },
188
+ onSaveError(e) {
189
+ console.error("SAVE ERROR", e)
190
+ if(toast && saveDraftErrorToast)
191
+ toast.add({ severity: 'error', summary: saveErrorToast, detail: e.message ?? e, life: 5000 })
192
+ onSaveError(e)
193
+ }
194
+ })
195
+
196
+ return {
197
+ value: synchronizedData.value,
198
+ changed: synchronizedData.changed,
199
+ save: synchronizedData.save,
200
+ saving: synchronizedData.saving,
201
+ model,
202
+ }
203
+
204
+ }
205
+ })
19
206
 
20
207
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/frontend-auto-form",
3
- "version": "0.8.111",
3
+ "version": "0.8.113",
4
4
  "scripts": {
5
5
  "memDev": "node server/start.js memDev --enableSessions --initScript ./init.js --dbAccess",
6
6
  "localDevInit": "rm tmp.db; lcli localDev --enableSessions --initScript ./init.js",
@@ -22,16 +22,16 @@
22
22
  "type": "module",
23
23
  "dependencies": {
24
24
  "@fortawesome/fontawesome-free": "^6.5.2",
25
- "@live-change/cli": "^0.8.111",
26
- "@live-change/dao": "^0.8.111",
27
- "@live-change/dao-vue3": "^0.8.111",
28
- "@live-change/dao-websocket": "^0.8.111",
29
- "@live-change/framework": "^0.8.111",
30
- "@live-change/image-frontend": "^0.8.111",
31
- "@live-change/image-service": "^0.8.111",
32
- "@live-change/session-service": "^0.8.111",
33
- "@live-change/vue3-components": "^0.8.111",
34
- "@live-change/vue3-ssr": "^0.8.111",
25
+ "@live-change/cli": "^0.8.113",
26
+ "@live-change/dao": "^0.8.113",
27
+ "@live-change/dao-vue3": "^0.8.113",
28
+ "@live-change/dao-websocket": "^0.8.113",
29
+ "@live-change/framework": "^0.8.113",
30
+ "@live-change/image-frontend": "^0.8.113",
31
+ "@live-change/image-service": "^0.8.113",
32
+ "@live-change/session-service": "^0.8.113",
33
+ "@live-change/vue3-components": "^0.8.113",
34
+ "@live-change/vue3-ssr": "^0.8.113",
35
35
  "@vueuse/core": "^10.11.0",
36
36
  "codeceptjs-assert": "^0.0.5",
37
37
  "compression": "^1.7.4",
@@ -52,7 +52,7 @@
52
52
  "vue3-scroll-border": "0.1.6"
53
53
  },
54
54
  "devDependencies": {
55
- "@live-change/codeceptjs-helper": "^0.8.111",
55
+ "@live-change/codeceptjs-helper": "^0.8.113",
56
56
  "codeceptjs": "^3.6.5",
57
57
  "generate-password": "1.7.1",
58
58
  "playwright": "^1.41.2",
@@ -63,5 +63,5 @@
63
63
  "author": "Michał Łaszczewski <michal@laszczewski.pl>",
64
64
  "license": "ISC",
65
65
  "description": "",
66
- "gitHead": "9b96afb2fc61ab3d2a5d143924e2c56d411280b4"
66
+ "gitHead": "80dd8415e06d849cc00a7711c635a7388143ce1d"
67
67
  }