forked from galaxyproject/galaxy
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request galaxyproject#14892 from ElectronicBlueberry/reord…
…er-repeat Make form repeat blocks reordarable
- Loading branch information
Showing
13 changed files
with
342 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
<script setup lang="ts"> | ||
import { library } from "@fortawesome/fontawesome-svg-core"; | ||
import { faCaretDown, faCaretUp, faPlus, faTrashAlt } from "@fortawesome/free-solid-svg-icons"; | ||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; | ||
import { defineAsyncComponent, nextTick, type PropType } from "vue"; | ||
import { useKeyedObjects } from "@/composables/keyedObjects"; | ||
import FormCard from "./FormCard.vue"; | ||
const FormNode = defineAsyncComponent(() => import("./FormInputs.vue")); | ||
interface Input { | ||
name: string; | ||
title: string; | ||
help?: string; | ||
cache: Array<Record<string, unknown>>; | ||
} | ||
const props = defineProps({ | ||
input: { | ||
type: Object as PropType<Input>, | ||
required: true, | ||
}, | ||
sustainRepeats: { | ||
type: Boolean, | ||
default: false, | ||
}, | ||
passthroughProps: { | ||
type: Object, | ||
required: true, | ||
}, | ||
prefix: { | ||
type: String, | ||
default: null, | ||
}, | ||
}); | ||
const emit = defineEmits<{ | ||
(e: "insert"): void; | ||
(e: "delete", index: number): void; | ||
(e: "swap", a: number, b: number): void; | ||
}>(); | ||
// @ts-ignore: bad library types | ||
library.add(faPlus, faTrashAlt, faCaretUp, faCaretDown); | ||
function onInsert() { | ||
emit("insert"); | ||
} | ||
function onDelete(index: number) { | ||
emit("delete", index); | ||
} | ||
function getPrefix(index: number) { | ||
const name = `${props.input.name}_${index}`; | ||
if (props.prefix) { | ||
return `${props.prefix}|${name}`; | ||
} else { | ||
return name; | ||
} | ||
} | ||
function getTitle(index: number) { | ||
return `${index + 1}: ${props.input.title}`; | ||
} | ||
/** swap blocks if possible */ | ||
async function swap(index: number, swapWith: number, direction: "up" | "down") { | ||
if (swapWith >= 0 && swapWith < props.input.cache?.length) { | ||
emit("swap", index, swapWith); | ||
await nextTick(); | ||
const buttonId = getButtonId(swapWith, direction); | ||
document.getElementById(buttonId)?.focus(); | ||
} | ||
} | ||
/** get a uid for the up/down button */ | ||
function getButtonId(index: number, direction: "up" | "down") { | ||
const prefix = getPrefix(index); | ||
return `${prefix}_${direction}`; | ||
} | ||
const { keyObject } = useKeyedObjects(); | ||
</script> | ||
|
||
<template> | ||
<div> | ||
<div v-if="!props.sustainRepeats || props.input.cache?.length > 0"> | ||
<div class="font-weight-bold mb-2">{{ props.input.title }}</div> | ||
<div v-if="props.input.help" class="mb-2" data-description="repeat help">{{ props.input.help }}</div> | ||
</div> | ||
<FormCard | ||
v-for="(cache, cacheId) in props.input.cache" | ||
:key="keyObject(cache)" | ||
data-description="repeat block" | ||
class="card" | ||
:title="getTitle(cacheId)"> | ||
<template v-slot:operations> | ||
<span v-if="!props.sustainRepeats" class="float-right"> | ||
<b-button-group> | ||
<b-button | ||
:id="getButtonId(cacheId, 'up')" | ||
v-b-tooltip.hover.bottom | ||
title="move up" | ||
role="button" | ||
variant="link" | ||
size="sm" | ||
class="ml-0" | ||
@click="() => swap(cacheId, cacheId - 1, 'up')"> | ||
<FontAwesomeIcon icon="caret-up" /> | ||
</b-button> | ||
<b-button | ||
:id="getButtonId(cacheId, 'down')" | ||
v-b-tooltip.hover.bottom | ||
title="move down" | ||
role="button" | ||
variant="link" | ||
size="sm" | ||
class="ml-0" | ||
@click="() => swap(cacheId, cacheId + 1, 'down')"> | ||
<FontAwesomeIcon icon="caret-down" /> | ||
</b-button> | ||
</b-button-group> | ||
<b-button | ||
v-b-tooltip.hover.bottom | ||
title="delete" | ||
role="button" | ||
variant="link" | ||
size="sm" | ||
class="ml-0" | ||
@click="() => onDelete(cacheId)"> | ||
<FontAwesomeIcon icon="trash-alt" /> | ||
</b-button> | ||
</span> | ||
</template> | ||
<template v-slot:body> | ||
<FormNode v-bind="props.passthroughProps" :inputs="cache" :prefix="getPrefix(cacheId)" /> | ||
</template> | ||
</FormCard> | ||
<b-button v-if="!props.sustainRepeats" @click="onInsert"> | ||
<FontAwesomeIcon icon="plus" class="mr-1" /> | ||
<span data-description="repeat insert">Insert {{ props.input.title || "Repeat" }}</span> | ||
</b-button> | ||
</div> | ||
</template> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import { useKeyedObjects } from "./keyedObjects"; | ||
|
||
describe("useKeyedObjects", () => { | ||
it("returns the same id for the same object", () => { | ||
const { keyObject } = useKeyedObjects(); | ||
|
||
const obj = { | ||
a: 1, | ||
b: 2, | ||
} as { | ||
a: number; | ||
b: number; | ||
c?: number; | ||
}; | ||
|
||
const keyA = keyObject(obj); | ||
expect(keyObject(obj)).toBe(keyA); | ||
|
||
obj.a += 5; | ||
obj["c"] = 6; | ||
|
||
expect(keyObject(obj)).toBe(keyA); | ||
}); | ||
|
||
it("returns different ids for different objects", () => { | ||
const { keyObject } = useKeyedObjects(); | ||
|
||
const objA = { | ||
a: 1, | ||
}; | ||
|
||
const objB = { | ||
b: 2, | ||
}; | ||
|
||
const keyA = keyObject(objA); | ||
const keyB = keyObject(objB); | ||
expect(keyA).not.toBe(keyB); | ||
|
||
const objD = { | ||
d: 3, | ||
}; | ||
const objE = structuredClone(objD); | ||
const keyD = keyObject(objD); | ||
const keyE = keyObject(objE); | ||
expect(keyD).not.toBe(keyE); | ||
|
||
expect(keyObject({})).not.toBe(keyObject({})); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import { unref } from "vue"; | ||
|
||
import { useUid } from "./utils/uid"; | ||
|
||
/** | ||
* Allows for keying objects by their internal ids. | ||
* Returns a function which takes an object and returns a string uid. | ||
* Passing the same object to this function twice, will return the same id. | ||
* | ||
* Passing the same object to a function created by another | ||
* `useKeyedObjects` composable will not produce the same ids. | ||
* | ||
* A cloned object will not have the same id as the object it was cloned from. | ||
* Modifying an objects properties will not affect its id. | ||
* | ||
* @returns A function which allows for keying objects | ||
*/ | ||
export function useKeyedObjects() { | ||
const keyCache = new WeakMap<object, string>(); | ||
|
||
function keyObject(object: object) { | ||
const cachedKey = keyCache.get(object); | ||
|
||
if (cachedKey) { | ||
return cachedKey; | ||
} else { | ||
const key = unref(useUid("object-")); | ||
keyCache.set(object, key); | ||
return key; | ||
} | ||
} | ||
|
||
return { keyObject }; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.