@@ -39,14 +39,15 @@ import {
39
39
onMounted ,
40
40
ref ,
41
41
} from ' vue'
42
- import { useStore } from ' vuex'
43
- import { startCase , uniqueId } from ' lodash'
42
+ import { startCase , uniqueId } from ' lodash-es'
44
43
import WidgetComponent from ' @/components/cylc/workspace/Widget.vue'
45
- import LuminoWidget from ' @/components/cylc/workspace/lumino-widget '
44
+ import { LuminoWidget } from ' @/components/cylc/workspace/luminoWidget '
46
45
import { BoxPanel , DockPanel , Widget } from ' @lumino/widgets'
47
- import { when } from ' @/utils'
46
+ import { watchWithControl } from ' @/utils/reactivity'
47
+ import { replacer , reviver } from ' @/utils/json'
48
48
import { useDefaultView } from ' @/views/views'
49
49
import { eventBus } from ' @/services/eventBus'
50
+ import { useWorkspaceLayoutsCache } from ' @/composables/cacheStorage'
50
51
51
52
import ' @lumino/default-theme/style'
52
53
@@ -61,14 +62,12 @@ import '@lumino/default-theme/style'
61
62
*/
62
63
63
64
/**
64
- * Mitt event for adding a view to the workspace.
65
- * @typedef {Object} AddViewEvent
66
- * @property {string} name - the view to add
65
+ * Options for views in the workspace.
66
+ * @typedef {Object} IViewOptions
67
+ * @property {string} name - the view component name
67
68
* @property {Record<string,*>} initialOptions - prop passed to the view component
68
69
*/
69
70
70
- const $store = useStore ()
71
-
72
71
const props = defineProps ({
73
72
workflowName: {
74
73
type: String ,
@@ -98,7 +97,7 @@ const mainDiv = ref(null)
98
97
/**
99
98
* Mapping of widget ID to the name of view component and its initialOptions prop.
100
99
*
101
- * @type {import('vue').Ref<Map<string, AddViewEvent >>}
100
+ * @type {import('vue').Ref<Map<string, IViewOptions >>}
102
101
*/
103
102
const views = ref (new Map ())
104
103
@@ -115,21 +114,35 @@ const resizeObserver = new ResizeObserver(() => {
115
114
boxPanel .update ()
116
115
})
117
116
118
- onMounted (() => {
117
+ const layoutsCache = useWorkspaceLayoutsCache ()
118
+ const layoutWatcher = watchWithControl (views, saveLayout, { deep: true })
119
+
120
+ onMounted (async () => {
121
+ // Store any add-view events that occur before the layout is ready
122
+ // (e.g. when opening log view from command menu):
123
+ const bufferedAddViewEvents = []
124
+ eventBus .on (' add-view' , (e ) => bufferedAddViewEvents .push (e))
125
+
119
126
// Attach box panel to DOM:
120
127
Widget .attach (boxPanel, mainDiv .value )
121
128
// Watch for resize of the main element to trigger relayout:
122
129
resizeObserver .observe (mainDiv .value )
130
+
131
+ await getLayout ()
132
+ dockPanel .layoutModified .connect (() => layoutWatcher .trigger ())
133
+
134
+ eventBus .off (' add-view' )
123
135
eventBus .on (' add-view' , addView)
136
+ bufferedAddViewEvents .forEach ((e ) => addView (e))
124
137
eventBus .on (' lumino:deleted' , onWidgetDeleted)
125
- getLayout ( props . workflowName )
138
+ eventBus . on ( ' reset-workspace-layout ' , resetToDefault )
126
139
})
127
140
128
141
onBeforeUnmount (() => {
129
142
resizeObserver .disconnect ()
130
143
eventBus .off (' add-view' , addView)
131
144
eventBus .off (' lumino:deleted' , onWidgetDeleted)
132
- saveLayout ( )
145
+ eventBus . off ( ' reset-workspace-layout ' , resetToDefault )
133
146
// Register with Lumino that the dock panel is no longer used,
134
147
// otherwise uncaught errors can occur when restoring layout
135
148
dockPanel .dispose ()
@@ -138,85 +151,105 @@ onBeforeUnmount(() => {
138
151
/**
139
152
* Create a widget and add it to the dock.
140
153
*
141
- * @param {AddViewEvent} event
154
+ * @param {IViewOptions} param0
142
155
* @param {boolean} onTop
143
156
*/
144
- const addView = ({ name, initialOptions = {} }, onTop = true ) => {
157
+ async function addView ({ name, initialOptions = {} }, onTop = true ) {
158
+ layoutWatcher .pause ()
145
159
const id = uniqueId (' widget' )
146
160
const luminoWidget = new LuminoWidget (id, startCase (name), /* closable */ true )
147
161
dockPanel .addWidget (luminoWidget, { mode: ' tab-after' })
162
+ if (onTop) {
163
+ dockPanel .selectWidget (luminoWidget)
164
+ }
148
165
// give time for Lumino's widget DOM element to be created
149
- nextTick (() => {
150
- views .value .set (id, { name, initialOptions })
151
- if (onTop) {
152
- dockPanel .selectWidget (luminoWidget)
153
- }
154
- })
166
+ await nextTick ()
167
+ views .value .set (id, { name, initialOptions })
168
+ layoutWatcher .resume ()
169
+ layoutWatcher .trigger ()
155
170
}
156
171
157
172
/**
158
173
* Remove all the widgets present in the DockPanel.
159
174
*/
160
- const closeAllViews = () => {
175
+ async function closeAllViews () {
161
176
for (const widget of Array .from (dockPanel .widgets ())) {
162
177
widget .close ()
163
178
}
179
+ await nextTick ()
164
180
}
165
181
166
182
/**
167
- * Get the saved layout (if there is one) for the given workflow,
183
+ * Get the saved layout (if there is one) for the current workflow,
168
184
* else add the default view.
169
- *
170
- * @param {string} workflowName
171
185
*/
172
- const getLayout = ( workflowName ) => {
173
- restoreLayout (workflowName ) || addView ({ name: defaultView .value })
186
+ async function getLayout () {
187
+ await restoreLayout () || await addView ({ name: defaultView .value })
174
188
}
175
189
176
190
/**
177
- * Save the current layout/views to the store .
191
+ * Save the current layout/views to cache storage .
178
192
*/
179
- const saveLayout = () => {
180
- $store . commit ( ' app/saveLayout ' , {
181
- workflowName : props . workflowName ,
193
+ async function saveLayout () {
194
+ // Serialize layout first to synchronously capture the current state
195
+ const serializedLayout = JSON . stringify ({
182
196
layout: dockPanel .saveLayout (),
183
- views: new Map (views .value ),
184
- })
197
+ views: views .value ,
198
+ }, replacer)
199
+ const cache = await layoutsCache
200
+ // Overrides on FIFO basis:
201
+ await cache .put (props .workflowName , new Response (
202
+ serializedLayout,
203
+ { headers: { ' Content-Type' : ' application/json' } }
204
+ ))
205
+ const keys = await cache .keys ()
206
+ if (keys .length > 100 ) {
207
+ await cache .delete (keys[0 ])
208
+ }
185
209
}
186
210
187
211
/**
188
- * Restore the layout for this workflow from the store, if it was saved.
212
+ * Return the saved layout for this workflow from cache storage, if it was saved.
213
+ */
214
+ async function getStoredLayout () {
215
+ const cache = await layoutsCache
216
+ const stored = await cache .match (props .workflowName ).then ((r ) => r? .text ())
217
+ if (stored) {
218
+ return JSON .parse (
219
+ stored,
220
+ (key , value ) => reviver (key, LuminoWidget .layoutReviver (key, value))
221
+ )
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Restore the layout for this workflow, if it was saved.
189
227
*
190
- * @param {string} workflowName
191
- * @returns {boolean} true if the layout was restored, false otherwise
228
+ * @returns true if the layout was restored, false otherwise
192
229
*/
193
- const restoreLayout = ( workflowName ) => {
194
- const stored = $store . state . app . workspaceLayouts . get (workflowName )
230
+ async function restoreLayout () {
231
+ const stored = await getStoredLayout ( )
195
232
if (stored) {
233
+ layoutWatcher .pause ()
196
234
dockPanel .restoreLayout (stored .layout )
197
235
// Wait for next tick so that Lumino has created the widget divs that the
198
236
// views will be teleported into
199
- nextTick (() => {
200
- views .value = stored .views
201
- } )
237
+ await nextTick ()
238
+ views .value = stored .views
239
+ layoutWatcher . resume ( )
202
240
return true
203
241
}
204
242
return false
205
243
}
206
244
207
245
/**
208
- * Save & close the current layout and open the one for the given workflow.
209
- *
210
- * @param {string} workflowName
246
+ * Reset the workspace layout to a single tab with the default view.
211
247
*/
212
- const changeLayout = (workflowName ) => {
213
- saveLayout ()
214
- closeAllViews ()
215
- // Wait if necessary for the workflowName prop to be updated to the new value:
216
- when (
217
- () => props .workflowName === workflowName,
218
- () => getLayout (workflowName),
219
- )
248
+ async function resetToDefault () {
249
+ layoutWatcher .pause ()
250
+ await closeAllViews ()
251
+ await addView ({ name: defaultView .value })
252
+ // (addView resumes the layout watcher)
220
253
}
221
254
222
255
/**
@@ -225,13 +258,12 @@ const changeLayout = (workflowName) => {
225
258
* @param {string} id - widget ID
226
259
*/
227
260
const onWidgetDeleted = (id ) => {
261
+ // layoutWatcher will be triggered by DockPanel.layoutModified, so pause to avoid duplicate trigger:
262
+ layoutWatcher .pause ()
228
263
views .value .delete (id)
264
+ layoutWatcher .resume ()
229
265
if (! views .value .size ) {
230
266
emit (' emptied' )
231
267
}
232
268
}
233
-
234
- defineExpose ({
235
- changeLayout,
236
- })
237
269
< / script>
0 commit comments