@hamak/ui-store-impl 0.3.0 → 0.4.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.
- package/CHANGELOG.md +18 -0
- package/dist/es2015/fs/commands/fs-commands.js +311 -0
- package/dist/es2015/fs/commands/structure-commands.js +21 -0
- package/dist/es2015/fs/core/fs-adapter.js +123 -0
- package/dist/es2015/fs/core/fs-facade.js +78 -0
- package/dist/es2015/fs/index.js +9 -0
- package/dist/es2015/fs/utils/data-updater.js +244 -0
- package/dist/es2015/fs/utils/deep-equal.js +28 -0
- package/dist/es2015/index.js +1 -0
- package/dist/fs/commands/fs-commands.d.ts +96 -0
- package/dist/fs/commands/fs-commands.d.ts.map +1 -0
- package/dist/fs/commands/fs-commands.js +311 -0
- package/dist/fs/commands/structure-commands.d.ts +76 -0
- package/dist/fs/commands/structure-commands.d.ts.map +1 -0
- package/dist/fs/commands/structure-commands.js +21 -0
- package/dist/fs/core/fs-adapter.d.ts +57 -0
- package/dist/fs/core/fs-adapter.d.ts.map +1 -0
- package/dist/fs/core/fs-adapter.js +123 -0
- package/dist/fs/core/fs-facade.d.ts +37 -0
- package/dist/fs/core/fs-facade.d.ts.map +1 -0
- package/dist/fs/core/fs-facade.js +78 -0
- package/dist/fs/index.d.ts +7 -0
- package/dist/fs/index.d.ts.map +1 -0
- package/dist/fs/index.js +9 -0
- package/dist/fs/utils/data-updater.d.ts +36 -0
- package/dist/fs/utils/data-updater.d.ts.map +1 -0
- package/dist/fs/utils/data-updater.js +248 -0
- package/dist/fs/utils/deep-equal.d.ts +5 -0
- package/dist/fs/utils/deep-equal.d.ts.map +1 -0
- package/dist/fs/utils/deep-equal.js +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/package.json +8 -5
- package/src/fs/commands/fs-commands.ts +405 -0
- package/src/fs/commands/structure-commands.ts +105 -0
- package/src/fs/core/fs-adapter.ts +180 -0
- package/src/fs/core/fs-facade.ts +100 -0
- package/src/fs/index.ts +11 -0
- package/src/fs/utils/data-updater.ts +273 -0
- package/src/fs/utils/deep-equal.ts +35 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DirectoryNode,
|
|
3
|
+
FileNode,
|
|
4
|
+
FileSystemNode,
|
|
5
|
+
FileSystemState,
|
|
6
|
+
FileContentSchema,
|
|
7
|
+
fileSystemNodeInitialState,
|
|
8
|
+
FileSystemNodeState
|
|
9
|
+
} from '@hamak/ui-store-api';
|
|
10
|
+
import { Pathway, Holder } from '@hamak/shared-utils';
|
|
11
|
+
import { StructureNodeCommand } from './structure-commands';
|
|
12
|
+
import { DataUpdater } from '../utils/data-updater';
|
|
13
|
+
import { produce, current, isDraft, original } from 'immer';
|
|
14
|
+
import { deepEqual } from '../utils/deep-equal';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Base interface for filesystem commands
|
|
18
|
+
*/
|
|
19
|
+
export interface FsCommandBase {
|
|
20
|
+
name : 'mkdir' | 'set-file' | 'remove' | 'update-file-content' | 'set-file-content'
|
|
21
|
+
path : string | string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create directory command
|
|
26
|
+
*/
|
|
27
|
+
export interface MkdirCommand extends FsCommandBase{
|
|
28
|
+
name : 'mkdir'
|
|
29
|
+
parents? : boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Remove file or directory command
|
|
34
|
+
*/
|
|
35
|
+
export interface RemoveCommand extends FsCommandBase{
|
|
36
|
+
name : 'remove'
|
|
37
|
+
recursive? : boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Set file command (create or update)
|
|
42
|
+
*/
|
|
43
|
+
export interface SetFileCommand extends FsCommandBase{
|
|
44
|
+
name : 'set-file'
|
|
45
|
+
content : any
|
|
46
|
+
schema : FileContentSchema
|
|
47
|
+
override? : boolean
|
|
48
|
+
contentIsPresent? : boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Update file content with structure command
|
|
53
|
+
*/
|
|
54
|
+
export interface UpdateFileContentCommand extends FsCommandBase{
|
|
55
|
+
name : 'update-file-content'
|
|
56
|
+
contentCommand : StructureNodeCommand
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Set file content directly
|
|
61
|
+
*/
|
|
62
|
+
export interface SetFileContentCommand extends FsCommandBase{
|
|
63
|
+
name : 'set-file-content'
|
|
64
|
+
content : any,
|
|
65
|
+
fromRemote?: boolean
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Union type for all filesystem commands
|
|
70
|
+
*/
|
|
71
|
+
export type FileSystemCommand =
|
|
72
|
+
| MkdirCommand
|
|
73
|
+
| RemoveCommand
|
|
74
|
+
| SetFileCommand
|
|
75
|
+
| UpdateFileContentCommand
|
|
76
|
+
| SetFileContentCommand
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Convert path to steps array
|
|
80
|
+
*/
|
|
81
|
+
export function pathSteps(path : string | string[]) : string[] {
|
|
82
|
+
if(Array.isArray(path)){
|
|
83
|
+
return path
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return path.split('/').filter(s => s.trim().length > 0)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Get parent path steps
|
|
91
|
+
*/
|
|
92
|
+
export function parentPathSteps(path : string | string[]) : string[] {
|
|
93
|
+
const steps = pathSteps(path)
|
|
94
|
+
return steps.length > 0 ? steps.slice(0, steps.length - 1) : steps
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get filesystem node at path
|
|
99
|
+
*/
|
|
100
|
+
export function getFileSystemNode(fsNode: FileSystemNode, path : string | string[]) : FileSystemNode | undefined {
|
|
101
|
+
const steps = pathSteps(path)
|
|
102
|
+
let result : FileSystemNode | undefined = fsNode
|
|
103
|
+
for(let i = 0; i < steps.length; i++) {
|
|
104
|
+
const step = steps[i]
|
|
105
|
+
if (result === undefined){
|
|
106
|
+
return undefined
|
|
107
|
+
} else if(result.type === 'directory'){
|
|
108
|
+
result = result.children[step]
|
|
109
|
+
} else {
|
|
110
|
+
return undefined
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create a directory node
|
|
119
|
+
*/
|
|
120
|
+
function createDirectoryNode(step: string) : DirectoryNode{
|
|
121
|
+
return {
|
|
122
|
+
type: 'directory',
|
|
123
|
+
state: fileSystemNodeInitialState(),
|
|
124
|
+
name: step,
|
|
125
|
+
children: {}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Create a file node
|
|
131
|
+
*/
|
|
132
|
+
function createFileNode(name: string, content : any, schema : FileContentSchema, state : FileSystemNodeState) : FileNode{
|
|
133
|
+
return {type: 'file', name, content, schema, state};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* FileSystem command handler - executes filesystem operations
|
|
138
|
+
*/
|
|
139
|
+
export class FileSystemCommandHandler {
|
|
140
|
+
readonly contentCommandHandler = new FileContentCommandHandler()
|
|
141
|
+
|
|
142
|
+
execute(state: FileSystemState, command: FileSystemCommand): FileSystemState {
|
|
143
|
+
switch (command.name) {
|
|
144
|
+
case 'mkdir': return this.executeMkdir(state, command)
|
|
145
|
+
case 'set-file': return this.executeSetFile(state, command)
|
|
146
|
+
case 'update-file-content' : return this.executeContentCommand(state, command)
|
|
147
|
+
case 'set-file-content' : return this.executeSetContentCommand(state, command)
|
|
148
|
+
case 'remove': return this.executeRemove(state, command)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private executeMkdir(state: FileSystemState, command : MkdirCommand) : FileSystemState{
|
|
153
|
+
const {path, parents} = command
|
|
154
|
+
const steps = pathSteps(path)
|
|
155
|
+
|
|
156
|
+
return produce(state, draft => {
|
|
157
|
+
const {root} = draft
|
|
158
|
+
let dir = root
|
|
159
|
+
for(let i = 0; i < steps.length; i++){
|
|
160
|
+
const step = steps[i]
|
|
161
|
+
if(i + 1 === steps.length){
|
|
162
|
+
// Last step is the element to create
|
|
163
|
+
if(dir.children[step] === undefined){
|
|
164
|
+
dir.children[step] = createDirectoryNode(step)
|
|
165
|
+
}
|
|
166
|
+
}else {
|
|
167
|
+
let child = dir.children[step]
|
|
168
|
+
if(child === undefined){
|
|
169
|
+
if(parents === true){
|
|
170
|
+
child = createDirectoryNode(step)
|
|
171
|
+
dir.children[step] = child
|
|
172
|
+
}else {
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
}else if(child.type === 'directory'){
|
|
176
|
+
dir = child
|
|
177
|
+
}else {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
dir = child
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
private executeRemove(state: FileSystemState, command : RemoveCommand) : FileSystemState{
|
|
189
|
+
const {path, recursive} = command
|
|
190
|
+
const steps = pathSteps(path)
|
|
191
|
+
|
|
192
|
+
return produce(state, draft => {
|
|
193
|
+
const {root} = draft
|
|
194
|
+
const parentDir = getFileSystemNode(root, steps.slice(0, steps.length - 1))
|
|
195
|
+
if(parentDir === undefined ){
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if(parentDir.type === 'directory'){
|
|
200
|
+
const last = steps[steps.length - 1]
|
|
201
|
+
const child = parentDir.children[last]
|
|
202
|
+
if(child?.type === 'directory'
|
|
203
|
+
&& Object.keys(child.children).length > 0
|
|
204
|
+
&& !(recursive === true) ){
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
delete parentDir.children[last]
|
|
209
|
+
}
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private executeSetFile(state: FileSystemState, command : SetFileCommand) : FileSystemState{
|
|
214
|
+
const {path, content, schema, override, contentIsPresent} = command
|
|
215
|
+
const steps = pathSteps(path)
|
|
216
|
+
|
|
217
|
+
return produce(state, draft => {
|
|
218
|
+
const {root} = draft
|
|
219
|
+
const parentPath = steps.slice(0, steps.length - 1);
|
|
220
|
+
const parentDir = getFileSystemNode(root, parentPath)
|
|
221
|
+
if(parentDir === undefined ){
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if(parentDir.type === 'directory'){
|
|
226
|
+
const last = steps[steps.length - 1]
|
|
227
|
+
const child = parentDir.children[last]
|
|
228
|
+
if(child === undefined || override === true){
|
|
229
|
+
parentDir.children[last] = createFileNode(last, content, schema, fileSystemNodeInitialState(contentIsPresent))
|
|
230
|
+
} else {
|
|
231
|
+
console.warn(`File already exists at location`, path)
|
|
232
|
+
}
|
|
233
|
+
}else{
|
|
234
|
+
console.warn(`Parent file is not directory`, parentPath)
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
private executeContentCommand(state: FileSystemState, command : UpdateFileContentCommand) : FileSystemState{
|
|
241
|
+
const {path, contentCommand} = command
|
|
242
|
+
|
|
243
|
+
return produce(state, draft => {
|
|
244
|
+
const {root} = draft
|
|
245
|
+
const fileNode = getFileSystemNode(root, path)
|
|
246
|
+
|
|
247
|
+
if(fileNode !== undefined){
|
|
248
|
+
if(fileNode.type === 'file'){
|
|
249
|
+
this.contentCommandHandler.execute(fileNode, contentCommand);
|
|
250
|
+
|
|
251
|
+
const previousFile = getFileSystemNode(state.root, path)
|
|
252
|
+
const previousContent = previousFile?.type === "file" ? previousFile.content : undefined
|
|
253
|
+
|
|
254
|
+
this.contentMayChange(fileNode);
|
|
255
|
+
}else{
|
|
256
|
+
console.warn(`Not a file at location`, path)
|
|
257
|
+
}
|
|
258
|
+
}else{
|
|
259
|
+
console.warn(`No file found at location`, path)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
private contentMayChange(fileNode: FileNode) {
|
|
267
|
+
const changed = ! deepEqual(this.original(fileNode.content), this.current(fileNode.content))
|
|
268
|
+
if(changed){
|
|
269
|
+
if (fileNode.state === undefined) {
|
|
270
|
+
fileNode.state = fileSystemNodeInitialState()
|
|
271
|
+
}
|
|
272
|
+
fileNode.state.contentHistory.push(this.original(fileNode.content))
|
|
273
|
+
|
|
274
|
+
// check compared to memo modification status
|
|
275
|
+
if (fileNode.state.memo !== undefined) {
|
|
276
|
+
const memo = fileNode.state.memo
|
|
277
|
+
memo.modified = !deepEqual(this.current(memo.originalContent), this.current(fileNode.content))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private executeSetContentCommand(state: FileSystemState, command : SetFileContentCommand) : FileSystemState{
|
|
283
|
+
const {path, content, fromRemote} = command
|
|
284
|
+
|
|
285
|
+
return produce(state, draft => {
|
|
286
|
+
const {root} = draft
|
|
287
|
+
const fileNode = getFileSystemNode(root, path)
|
|
288
|
+
|
|
289
|
+
if(fileNode !== undefined){
|
|
290
|
+
if(fileNode.type === 'file'){
|
|
291
|
+
fileNode.content = content
|
|
292
|
+
if(fileNode.state === undefined){
|
|
293
|
+
fileNode.state = fileSystemNodeInitialState(true)
|
|
294
|
+
}
|
|
295
|
+
if(fromRemote){
|
|
296
|
+
fileNode.state.contentLoaded = true
|
|
297
|
+
if(fileNode.state.memo === undefined){
|
|
298
|
+
fileNode.state.memo = {originalContent: {value: content}, modified: false}
|
|
299
|
+
} else {
|
|
300
|
+
const memo = fileNode.state.memo
|
|
301
|
+
memo.originalContent = {value: content}
|
|
302
|
+
memo.modified = false
|
|
303
|
+
}
|
|
304
|
+
} else {
|
|
305
|
+
this.contentMayChange(fileNode)
|
|
306
|
+
}
|
|
307
|
+
}else{
|
|
308
|
+
console.warn(`Not a file at location`, path)
|
|
309
|
+
}
|
|
310
|
+
}else{
|
|
311
|
+
console.warn(`No file found at location`, path)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
})
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
protected current<T = any>(o : T):T{
|
|
318
|
+
if(o === null || o === undefined){
|
|
319
|
+
return o
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return isDraft(o) ? current(o) : o
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
protected original<T = any>(o : T):T | undefined{
|
|
326
|
+
if(o === null || o === undefined){
|
|
327
|
+
return o
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return isDraft(o) ? original(o) : o
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* File content command handler - handles structure commands on file content
|
|
336
|
+
*/
|
|
337
|
+
export class FileContentCommandHandler {
|
|
338
|
+
readonly updater = new DataUpdater()
|
|
339
|
+
|
|
340
|
+
execute(file : FileNode, command: StructureNodeCommand) {
|
|
341
|
+
switch (command.name) {
|
|
342
|
+
case 'add-at':{
|
|
343
|
+
this.executeAdd(file, command)
|
|
344
|
+
}break
|
|
345
|
+
case 'insert-at':{
|
|
346
|
+
this.executeInsert(file, command)
|
|
347
|
+
}break
|
|
348
|
+
case 'set-at':{
|
|
349
|
+
this.executeSet(file, command)
|
|
350
|
+
} break
|
|
351
|
+
case 'delete-at': {
|
|
352
|
+
this.executeDelete(file, command)
|
|
353
|
+
} break
|
|
354
|
+
case 'batch-update': {
|
|
355
|
+
this.executeBatch(file, command)
|
|
356
|
+
} break
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private executeDelete(file : FileNode, command: any) {
|
|
361
|
+
const {itinerary} = command
|
|
362
|
+
if (itinerary === undefined) {
|
|
363
|
+
file.content = undefined
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const {content} = file
|
|
367
|
+
this.updater.executeDelete(content, itinerary)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
private executeSet(file : FileNode, command: any) {
|
|
371
|
+
const {itinerary, value} = command
|
|
372
|
+
if (itinerary === undefined) {
|
|
373
|
+
file.content = value
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const {content} = file
|
|
377
|
+
this.updater.executeSet(content, itinerary, value)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private executeAdd(file: FileNode, command: any) {
|
|
381
|
+
const {itinerary, value} = command
|
|
382
|
+
|
|
383
|
+
const {content} = file
|
|
384
|
+
this.updater.executeAdd(content, itinerary, value)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
private executeInsert(file: FileNode, command: any) {
|
|
388
|
+
const {itinerary, value} = command
|
|
389
|
+
if (itinerary === undefined) {
|
|
390
|
+
return
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const {content} = file
|
|
394
|
+
this.updater.executeInsert(content, itinerary, value)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Executes a batch of commands, rebasing each on previous mutations.
|
|
399
|
+
*/
|
|
400
|
+
public executeBatch(file: FileNode, command: any): void {
|
|
401
|
+
const { content } = file;
|
|
402
|
+
this.updater.executeBatch(content, command);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Itinerary, StackElement } from '@hamak/shared-utils';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base interface for structure node commands
|
|
5
|
+
*/
|
|
6
|
+
export interface StructureNodeCommandBase {
|
|
7
|
+
name: "add-at" | "set-at" | "delete-at" | 'insert-at' | "batch-update"
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Base interface for unitary (single operation) commands
|
|
12
|
+
*/
|
|
13
|
+
export interface UnitaryStructureNodeCommandBase extends StructureNodeCommandBase {
|
|
14
|
+
name: "add-at" | "set-at" | "delete-at" | 'insert-at'
|
|
15
|
+
itinerary: Itinerary
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Base interface for commands that insert/update values
|
|
20
|
+
*/
|
|
21
|
+
export interface StructureNodeUpsertCommandBase extends UnitaryStructureNodeCommandBase{
|
|
22
|
+
name: "add-at" | "set-at" | 'insert-at'
|
|
23
|
+
prototypes? : StackElement<any>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Add a value at the specified itinerary
|
|
28
|
+
*/
|
|
29
|
+
export interface AddStructureNodeCommand extends StructureNodeUpsertCommandBase {
|
|
30
|
+
name: "add-at"
|
|
31
|
+
value: any
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Insert a value at the specified position
|
|
36
|
+
*/
|
|
37
|
+
export interface InsertStructureNodeCommand extends StructureNodeUpsertCommandBase {
|
|
38
|
+
name: "insert-at"
|
|
39
|
+
value: any
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set a value at the specified itinerary
|
|
44
|
+
*/
|
|
45
|
+
export interface SetStructureNodeCommand extends StructureNodeUpsertCommandBase {
|
|
46
|
+
name: "set-at"
|
|
47
|
+
value: any
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Delete a value at the specified itinerary
|
|
52
|
+
*/
|
|
53
|
+
export interface DeleteStructureNodeCommand extends UnitaryStructureNodeCommandBase {
|
|
54
|
+
name: "delete-at"
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Batch update multiple operations
|
|
59
|
+
*/
|
|
60
|
+
export interface BatchUpdateStructureNodeCommand extends StructureNodeCommandBase {
|
|
61
|
+
name: "batch-update"
|
|
62
|
+
autoRebase?: boolean
|
|
63
|
+
commands: UnitaryStructureNodeCommand[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Union type for unitary structure node commands
|
|
68
|
+
*/
|
|
69
|
+
export type UnitaryStructureNodeCommand =
|
|
70
|
+
| AddStructureNodeCommand
|
|
71
|
+
| InsertStructureNodeCommand
|
|
72
|
+
| SetStructureNodeCommand
|
|
73
|
+
| DeleteStructureNodeCommand
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Union type for all structure node commands
|
|
77
|
+
*/
|
|
78
|
+
export type StructureNodeCommand = UnitaryStructureNodeCommand | BatchUpdateStructureNodeCommand
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Helper class for creating structure node commands
|
|
82
|
+
*/
|
|
83
|
+
export class StructureNodeCommandHelper {
|
|
84
|
+
addNode(itinerary : Itinerary, value : any, prototypes?: StackElement<any>) : AddStructureNodeCommand{
|
|
85
|
+
return {name:'add-at', itinerary, value, prototypes}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
insertNode(itinerary : Itinerary, value : any, prototypes?: StackElement<any>) : InsertStructureNodeCommand{
|
|
89
|
+
return {name:'insert-at', itinerary, value, prototypes}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
setNode(itinerary : Itinerary, value : any, prototypes?: StackElement<any>) : SetStructureNodeCommand{
|
|
93
|
+
return {name:'set-at', itinerary, value, prototypes}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
deleteNode(itinerary : Itinerary) : DeleteStructureNodeCommand{
|
|
97
|
+
return {name:'delete-at', itinerary}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
batchUpdate(commands : UnitaryStructureNodeCommand[], autoRebase = false) : BatchUpdateStructureNodeCommand{
|
|
101
|
+
return {name:'batch-update', commands, autoRebase}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const structs = new StructureNodeCommandHelper()
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { Action, createSelector } from '@reduxjs/toolkit';
|
|
2
|
+
import {
|
|
3
|
+
FileSystemCommand,
|
|
4
|
+
FileSystemCommandHandler,
|
|
5
|
+
pathSteps
|
|
6
|
+
} from '../commands/fs-commands';
|
|
7
|
+
import {
|
|
8
|
+
FileSystemNode,
|
|
9
|
+
fileSystemNodeInitialState,
|
|
10
|
+
FileSystemState,
|
|
11
|
+
FileContentSchema,
|
|
12
|
+
FileSystemNodeActionParams,
|
|
13
|
+
Selector
|
|
14
|
+
} from '@hamak/ui-store-api';
|
|
15
|
+
import { StructureNodeCommand } from '../commands/structure-commands';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Action interface for filesystem operations
|
|
19
|
+
*/
|
|
20
|
+
export interface FileSystemNodeAction extends Action {
|
|
21
|
+
command: FileSystemCommand
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Factory function to create a FileSystemAdapter
|
|
26
|
+
*/
|
|
27
|
+
export function createFileSystemAdapter(sliceName: string) {
|
|
28
|
+
return new FileSystemAdapter(sliceName)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Filesystem reducer function type
|
|
33
|
+
*/
|
|
34
|
+
export type FileSystemNodeReducerFn = (state: FileSystemState, action: Action) => FileSystemState
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* FileSystemAdapter - Manages Redux state for virtual filesystem
|
|
38
|
+
*/
|
|
39
|
+
export class FileSystemAdapter {
|
|
40
|
+
readonly actions: FileSystemNodeActions
|
|
41
|
+
readonly commandHandler: FileSystemCommandHandler
|
|
42
|
+
readonly reducer: FileSystemNodeReducerFn
|
|
43
|
+
|
|
44
|
+
public constructor(public readonly sliceName: string) {
|
|
45
|
+
this.actions = new FileSystemNodeActions(sliceName)
|
|
46
|
+
this.commandHandler = new FileSystemCommandHandler()
|
|
47
|
+
|
|
48
|
+
this.reducer = (state= this.getInitialState(), action) => {
|
|
49
|
+
if (this.actions.isFileSystemNodeAction(action)) {
|
|
50
|
+
return this.commandHandler.execute(state, action.command)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return state
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getInitialState(): FileSystemState {
|
|
58
|
+
return {root:{type: 'directory', name:'', children:{}, state: fileSystemNodeInitialState()}}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
getActions() {
|
|
62
|
+
return this.actions
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
getReducer(extraReducers?: FileSystemNodeReducerFn): FileSystemNodeReducerFn {
|
|
66
|
+
if (extraReducers === undefined) {
|
|
67
|
+
return this.reducer
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (state, action) => this.reducer(extraReducers(state, action), action)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
createSelector<S>(fileSystemSelector: Selector<S, FileSystemState | undefined>, path : string | string[]) : Selector<S, FileSystemNode | undefined>{
|
|
74
|
+
const rootSelector = createSelector(fileSystemSelector, fs => fs?.root)
|
|
75
|
+
const steps = pathSteps(path)
|
|
76
|
+
if(steps === undefined || steps.length === 0){
|
|
77
|
+
return rootSelector
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return steps.reduce((acc, step) => {
|
|
81
|
+
return createSelector(acc, o => {
|
|
82
|
+
if(o === undefined || o.type === 'file'){
|
|
83
|
+
return o
|
|
84
|
+
}else {
|
|
85
|
+
return o.children[step]
|
|
86
|
+
}
|
|
87
|
+
})
|
|
88
|
+
}, rootSelector as Selector<S, FileSystemNode | undefined>)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* FileSystemNodeActions - Action creators for filesystem operations
|
|
94
|
+
*/
|
|
95
|
+
export class FileSystemNodeActions {
|
|
96
|
+
private readonly _mkdirType: string;
|
|
97
|
+
private readonly _setFileType: string;
|
|
98
|
+
private readonly _removeType: string;
|
|
99
|
+
private readonly _updateFileContentType: string;
|
|
100
|
+
private readonly _setFileContentType: string;
|
|
101
|
+
private actionTypeSet: Set<string>
|
|
102
|
+
|
|
103
|
+
constructor(public sliceName: string) {
|
|
104
|
+
this._mkdirType = `${this.sliceName}/createDirectoryNode`
|
|
105
|
+
this._setFileType = `${this.sliceName}/createFileNode`
|
|
106
|
+
this._removeType = `${this.sliceName}/removeNode`
|
|
107
|
+
this._updateFileContentType = `${this.sliceName}/updateFileContent`
|
|
108
|
+
this._setFileContentType = `${this.sliceName}/setFileContent`
|
|
109
|
+
|
|
110
|
+
this.actionTypeSet = new Set([
|
|
111
|
+
this._mkdirType,
|
|
112
|
+
this._setFileType,
|
|
113
|
+
this._removeType,
|
|
114
|
+
this._updateFileContentType,
|
|
115
|
+
this._setFileContentType
|
|
116
|
+
])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
get mkdirType(): string {
|
|
120
|
+
return this._mkdirType;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
get setFileType(): string {
|
|
124
|
+
return this._setFileType;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
get removeType(): string {
|
|
128
|
+
return this._removeType;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get updateFileContentType(): string {
|
|
132
|
+
return this._updateFileContentType;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
get setFileContentType(): string {
|
|
136
|
+
return this._setFileContentType;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
isFileSystemNodeAction(action: Action): action is FileSystemNodeAction {
|
|
140
|
+
return this.actionTypeSet.has(action.type)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
public mkdir(path: string | string[], parents?:boolean): FileSystemNodeAction {
|
|
144
|
+
return {
|
|
145
|
+
type: this.mkdirType,
|
|
146
|
+
command: {name : 'mkdir', path, parents}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
public setFile(path: string | string[], content:any, schema : FileContentSchema, params : FileSystemNodeActionParams = {override: true, contentIsPresent:true}): FileSystemNodeAction {
|
|
151
|
+
const {override, contentIsPresent} = params
|
|
152
|
+
return {
|
|
153
|
+
type: this.setFileType,
|
|
154
|
+
command: {name: 'set-file', path, content, schema, override, contentIsPresent}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
public updateFileContent(path: string | string[], contentCommand : StructureNodeCommand ): FileSystemNodeAction {
|
|
159
|
+
return {
|
|
160
|
+
type: this.updateFileContentType,
|
|
161
|
+
command: {name: 'update-file-content', path, contentCommand}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public setFileContent(path: string | string[], content : any, fromRemote = false): FileSystemNodeAction {
|
|
166
|
+
return {
|
|
167
|
+
type: this.setFileContentType,
|
|
168
|
+
command: {name: 'set-file-content', path, content, fromRemote}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
public removeNode(path : string | string[], recursive? : boolean): FileSystemNodeAction {
|
|
174
|
+
return {
|
|
175
|
+
type: this.removeType,
|
|
176
|
+
command: {name: 'remove', path, recursive}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
}
|