dazscript-framework 1.0.36 → 1.0.38
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/package.json +1 -1
- package/src/helpers/custom-action-installer-bulk.test.ts +33 -0
- package/src/helpers/custom-action-installer-bulk.ts +22 -0
- package/src/helpers/custom-action-installer-helper.ts +60 -2
- package/src/helpers/node-helper.test.ts +29 -0
- package/src/helpers/node-helper.ts +1 -1
- package/src/helpers/string-helper.test.ts +12 -0
- package/src/helpers/string-helper.ts +11 -1
package/package.json
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { SETUP_BULK_ACTIONS, applySetupBulkSelection } from './custom-action-installer-bulk'
|
|
3
|
+
|
|
4
|
+
describe('setup bulk actions', () => {
|
|
5
|
+
it('uses concise install and uninstall labels', () => {
|
|
6
|
+
expect(SETUP_BULK_ACTIONS.installAll.label).toBe('Install All')
|
|
7
|
+
expect(SETUP_BULK_ACTIONS.uninstallAll.label).toBe('Uninstall All')
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('selects every installer entry for install all', () => {
|
|
11
|
+
const entries = [
|
|
12
|
+
{ selected: false },
|
|
13
|
+
{ selected: true },
|
|
14
|
+
{ selected: false }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
applySetupBulkSelection(entries, true)
|
|
18
|
+
|
|
19
|
+
expect(entries.map(entry => entry.selected)).toEqual([true, true, true])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('deselects every installer entry for uninstall all', () => {
|
|
23
|
+
const entries = [
|
|
24
|
+
{ selected: true },
|
|
25
|
+
{ selected: false },
|
|
26
|
+
{ selected: true }
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
applySetupBulkSelection(entries, false)
|
|
30
|
+
|
|
31
|
+
expect(entries.map(entry => entry.selected)).toEqual([false, false, false])
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type BulkSelectableEntry = {
|
|
2
|
+
selected: boolean
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export const SETUP_BULK_ACTIONS = {
|
|
6
|
+
installAll: {
|
|
7
|
+
label: 'Install All',
|
|
8
|
+
toolTip: 'Install every setup entry, ignoring the current search filter and checkbox state.',
|
|
9
|
+
whatsThis: 'Installs every script action in this setup dialog. This does not depend on which rows are checked or currently visible.'
|
|
10
|
+
},
|
|
11
|
+
uninstallAll: {
|
|
12
|
+
label: 'Uninstall All',
|
|
13
|
+
toolTip: 'Uninstall every setup entry, ignoring the current search filter and checkbox state.',
|
|
14
|
+
whatsThis: 'Removes every script action in this setup dialog from its menu and toolbar targets. This does not depend on which rows are checked or currently visible.'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const applySetupBulkSelection = <TEntry extends BulkSelectableEntry>(entries: TEntry[], selected: boolean): void => {
|
|
19
|
+
entries.forEach((entry) => {
|
|
20
|
+
entry.selected = selected
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -12,6 +12,7 @@ import { TreeNode } from '@dsf/lib/tree-node'
|
|
|
12
12
|
import { promptKeyboardShortcut } from '@dsf/shared/set-keyboard-shortcut'
|
|
13
13
|
import { readFromFile, saveToFile } from './file-helper'
|
|
14
14
|
import { getCanonicalInstallerEntry, toActionKey } from './custom-action-installer-entries'
|
|
15
|
+
import { SETUP_BULK_ACTIONS, applySetupBulkSelection } from './custom-action-installer-bulk'
|
|
15
16
|
import { canResetSetupShortcut, getDisplayedSetupShortcut, resetSetupShortcut, resetSetupShortcuts, setSetupShortcut, updateShortcutOverrideState } from './custom-action-installer-shortcuts'
|
|
16
17
|
import { getScriptPath } from './script-helper'
|
|
17
18
|
|
|
@@ -192,6 +193,7 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
192
193
|
private readonly refreshListEvent$ = new Observable<void>()
|
|
193
194
|
private listView: DzListView | null = null
|
|
194
195
|
private shortcutListView: DzListView | null = null
|
|
196
|
+
private bulkSelectionApplied = false
|
|
195
197
|
|
|
196
198
|
constructor(
|
|
197
199
|
private readonly entries: InstallerEntry[],
|
|
@@ -207,6 +209,7 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
207
209
|
this.builder.options({ resizable: true, width: 1100, height: 700 })
|
|
208
210
|
this.dialog.setAcceptButtonText('Apply')
|
|
209
211
|
this.dialog.setCancelButtonText('Cancel')
|
|
212
|
+
this.addBulkActionButtons()
|
|
210
213
|
|
|
211
214
|
if (this.shortcutEntries.length > 0) {
|
|
212
215
|
const add = this.add
|
|
@@ -219,7 +222,9 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
getSelections(): SetupSelection {
|
|
222
|
-
this.
|
|
225
|
+
if (!this.bulkSelectionApplied) {
|
|
226
|
+
this.syncSelectionsFromListView()
|
|
227
|
+
}
|
|
223
228
|
this.syncShortcutSelectionsFromListView()
|
|
224
229
|
return {
|
|
225
230
|
actions: this.entries,
|
|
@@ -227,6 +232,10 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
227
232
|
}
|
|
228
233
|
}
|
|
229
234
|
|
|
235
|
+
hasBulkSelectionApplied(): boolean {
|
|
236
|
+
return this.bulkSelectionApplied
|
|
237
|
+
}
|
|
238
|
+
|
|
230
239
|
private buildScriptsTab(): void {
|
|
231
240
|
const add = this.add
|
|
232
241
|
|
|
@@ -430,6 +439,34 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
430
439
|
})
|
|
431
440
|
}
|
|
432
441
|
|
|
442
|
+
private addBulkActionButtons(): void {
|
|
443
|
+
if (this.entries.length === 0) return
|
|
444
|
+
|
|
445
|
+
const installAllButton = this.createBulkActionButton(
|
|
446
|
+
SETUP_BULK_ACTIONS.installAll.label,
|
|
447
|
+
SETUP_BULK_ACTIONS.installAll.toolTip,
|
|
448
|
+
SETUP_BULK_ACTIONS.installAll.whatsThis
|
|
449
|
+
)
|
|
450
|
+
installAllButton.clicked.scriptConnect(() => this.applyBulkSelection(true))
|
|
451
|
+
this.dialog.addButton(installAllButton, 0)
|
|
452
|
+
|
|
453
|
+
const uninstallAllButton = this.createBulkActionButton(
|
|
454
|
+
SETUP_BULK_ACTIONS.uninstallAll.label,
|
|
455
|
+
SETUP_BULK_ACTIONS.uninstallAll.toolTip,
|
|
456
|
+
SETUP_BULK_ACTIONS.uninstallAll.whatsThis
|
|
457
|
+
)
|
|
458
|
+
uninstallAllButton.clicked.scriptConnect(() => this.confirmAndApplyBulkUninstall())
|
|
459
|
+
this.dialog.addButton(uninstallAllButton, 1)
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private createBulkActionButton(label: string, toolTip: string, whatsThis: string): DzPushButton {
|
|
463
|
+
const button = new DzPushButton(this.dialog)
|
|
464
|
+
button.text = label
|
|
465
|
+
button.toolTip = toolTip
|
|
466
|
+
button.whatsThis = whatsThis
|
|
467
|
+
return button
|
|
468
|
+
}
|
|
469
|
+
|
|
433
470
|
private getContextListItem(listView: DzListView, listItem: any): DzListViewItem | null {
|
|
434
471
|
if (this.isListViewItem(listItem)) return listItem
|
|
435
472
|
|
|
@@ -535,6 +572,24 @@ class InstallerSelectionDialog extends BasicDialog {
|
|
|
535
572
|
this.checkVisible(this.listView, onOff)
|
|
536
573
|
}
|
|
537
574
|
|
|
575
|
+
private applyBulkSelection(selected: boolean): void {
|
|
576
|
+
applySetupBulkSelection(this.entries, selected)
|
|
577
|
+
this.bulkSelectionApplied = true
|
|
578
|
+
this.dialog.close()
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private confirmAndApplyBulkUninstall(): void {
|
|
582
|
+
const response = MessageBox.question(
|
|
583
|
+
'Uninstall all scripts from menu and toolbar targets?',
|
|
584
|
+
'Uninstall All',
|
|
585
|
+
'Uninstall All',
|
|
586
|
+
'Cancel'
|
|
587
|
+
)
|
|
588
|
+
if (response !== 0) return
|
|
589
|
+
|
|
590
|
+
this.applyBulkSelection(false)
|
|
591
|
+
}
|
|
592
|
+
|
|
538
593
|
private syncSelectionsFromListView() {
|
|
539
594
|
if (!this.listView) return
|
|
540
595
|
|
|
@@ -609,7 +664,10 @@ const runDialog = (actions: CustomAction[], options: SetupDialogOptions): SetupS
|
|
|
609
664
|
const entries = buildEntries(actions)
|
|
610
665
|
const shortcutEntries = buildShortcutEntries(options.shortcuts)
|
|
611
666
|
const dialog = new InstallerSelectionDialog(entries, shortcutEntries, options)
|
|
612
|
-
|
|
667
|
+
const accepted = dialog.ok()
|
|
668
|
+
return accepted || dialog.hasBulkSelectionApplied()
|
|
669
|
+
? dialog.getSelections()
|
|
670
|
+
: null
|
|
613
671
|
}
|
|
614
672
|
|
|
615
673
|
const readShortcutBackup = (backupPath: string): ShortcutBackupFile => {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { isHairType } from './node-helper'
|
|
3
|
+
|
|
4
|
+
const getTypeForNode = vi.fn()
|
|
5
|
+
const isHairTypeAsset = vi.fn()
|
|
6
|
+
|
|
7
|
+
vi.mock('@dsf/core/global', () => ({
|
|
8
|
+
app: {
|
|
9
|
+
getAssetMgr: () => ({
|
|
10
|
+
getTypeForNode,
|
|
11
|
+
isHairType: isHairTypeAsset
|
|
12
|
+
})
|
|
13
|
+
},
|
|
14
|
+
sceneHelper: {}
|
|
15
|
+
}))
|
|
16
|
+
|
|
17
|
+
describe('node helper hair classification', () => {
|
|
18
|
+
it('treats eyelash content types as hair regardless of case', () => {
|
|
19
|
+
getTypeForNode.mockReturnValue('Follower/Attachment/Head/Face/Eyelashes')
|
|
20
|
+
isHairTypeAsset.mockReturnValue(false)
|
|
21
|
+
|
|
22
|
+
const node = {
|
|
23
|
+
iskindof: (type: string) => type === 'DzSkeleton',
|
|
24
|
+
getSkeleton: () => node
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
expect(isHairType(node as unknown as DzNode)).toBe(true)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -120,7 +120,7 @@ export const isHairType = (node: DzNode): boolean => {
|
|
|
120
120
|
const figure = getFigure(node)!
|
|
121
121
|
const assetMgr = app.getAssetMgr()
|
|
122
122
|
const type = assetMgr.getTypeForNode(figure)
|
|
123
|
-
return assetMgr.isHairType(type) || contains(type.valueOf(), 'Hair')
|
|
123
|
+
return assetMgr.isHairType(type) || contains(type.valueOf(), ['Hair', 'Eyelashes'], true)
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
export const isGeoShell = (node: DzNode): boolean => {
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { contains } from './string-helper'
|
|
3
|
+
|
|
4
|
+
describe('string contains helper', () => {
|
|
5
|
+
it('matches any requested token case-insensitively when requested', () => {
|
|
6
|
+
expect(contains('Follower/Attachment/Head/Face/Eyelashes', ['hair', 'eyelashes'], true)).toBe(true)
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('keeps matching case-sensitive by default', () => {
|
|
10
|
+
expect(contains('Follower/Attachment/Head/Face/Eyelashes', 'eyelashes')).toBe(false)
|
|
11
|
+
})
|
|
12
|
+
})
|
|
@@ -4,7 +4,17 @@ export const isNumeric = (value: string | number): boolean => {
|
|
|
4
4
|
!isNaN(Number(value.toString())))
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
export const contains = (source: string, search: string | string[]): boolean => {
|
|
7
|
+
export const contains = (source: string, search: string | string[], caseInsensitive: boolean = false): boolean => {
|
|
8
|
+
if (caseInsensitive) {
|
|
9
|
+
source = source.toLowerCase()
|
|
10
|
+
if (Array.isArray(search)) {
|
|
11
|
+
search = search.map(s => s.toLowerCase())
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
search = search.toLowerCase()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
if (Array.isArray(search)) {
|
|
9
19
|
for (const s of search) {
|
|
10
20
|
if (source.indexOf(s) >= 0) return true
|