mirror of
https://github.com/sysadminsmedia/homebox.git
synced 2025-12-24 06:28:34 +01:00
Creation modal quality of life changes (#467)
Co-authored-by: Matt Kilgore <tankerkiller125@users.noreply.github.com> Co-authored-by: Tonya <tonya@tokia.dev>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
<template>
|
||||
<div class="z-[999]">
|
||||
<input :id="modalId" v-model="modal" type="checkbox" class="modal-toggle" />
|
||||
<div class="modal modal-bottom overflow-visible sm:modal-middle">
|
||||
<div
|
||||
class="modal overflow-visible sm:modal-middle"
|
||||
:class="{ 'modal-bottom': !props.modalTop }"
|
||||
:modal-top="props.modalTop"
|
||||
>
|
||||
<div ref="modalBox" class="modal-box relative overflow-visible">
|
||||
<button
|
||||
v-if="props.showCloseButton"
|
||||
@@ -41,6 +45,14 @@
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
clickOutsideToClose: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
modalTop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const modalBox = ref();
|
||||
@@ -51,9 +63,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (props.clickOutsideToClose) {
|
||||
onClickOutside(modalBox, () => {
|
||||
close();
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
if (props.readonly) {
|
||||
@@ -74,3 +88,23 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
@media (max-width: 640px) {
|
||||
.modal[modal-top=true] {
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.modal[modal-top=true] :where(.modal-box) {
|
||||
max-width: none;
|
||||
--tw-translate-y: 2.5rem /* 40px */;
|
||||
--tw-scale-x: 1;
|
||||
--tw-scale-y: 1;
|
||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate))
|
||||
skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||
width: 100%;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<span class="label-text">{{ label }}</span>
|
||||
</label>
|
||||
<div class="dropdown dropdown-top sm:dropdown-end">
|
||||
<div tabindex="0" class="flex min-h-[48px] w-full flex-wrap gap-2 rounded-lg border border-gray-400 p-4">
|
||||
<div tabindex="0" class="flex min-h-[48px] w-full flex-wrap gap-2 rounded-lg border border-base-content/20 p-4">
|
||||
<span v-for="itm in value" :key="itm.id" class="badge">
|
||||
{{ itm.name }}
|
||||
</span>
|
||||
@@ -20,7 +20,7 @@
|
||||
<div
|
||||
tabindex="0"
|
||||
style="display: inline"
|
||||
class="dropdown-content menu z-[9999] mb-1 w-full rounded border border-gray-400 bg-base-100 shadow"
|
||||
class="dropdown-content menu z-[9999] mb-1 w-full rounded border border-base-content/20 bg-base-100 shadow"
|
||||
>
|
||||
<div class="m-2">
|
||||
<input v-model="search" placeholder="Search…" class="input input-bordered input-sm w-full" />
|
||||
|
||||
@@ -141,10 +141,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
whenever(
|
||||
watch(
|
||||
() => modal.value,
|
||||
() => {
|
||||
open => {
|
||||
if (open) {
|
||||
useTimeoutFn(() => {
|
||||
focused.value = true;
|
||||
}, 50);
|
||||
|
||||
if (locationId.value) {
|
||||
const found = locations.value.find(l => l.id === locationId.value);
|
||||
@@ -156,6 +159,9 @@
|
||||
if (labelId.value) {
|
||||
form.labels = labels.value.filter(l => l.id === labelId.value);
|
||||
}
|
||||
} else {
|
||||
focused.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -64,10 +64,14 @@
|
||||
loading.value = false;
|
||||
}
|
||||
|
||||
whenever(
|
||||
watch(
|
||||
() => modal.value,
|
||||
() => {
|
||||
open => {
|
||||
if (open)
|
||||
useTimeoutFn(() => {
|
||||
focused.value = true;
|
||||
}, 50);
|
||||
else focused.value = false;
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<BaseModal v-model="modal">
|
||||
<template #title>{{ $t("components.location.create_modal.title") }}</template>
|
||||
<form @submit.prevent="create()">
|
||||
<LocationSelector v-model="form.parent" />
|
||||
<FormTextField
|
||||
ref="locationNameRef"
|
||||
v-model="form.name"
|
||||
@@ -17,7 +18,6 @@
|
||||
:label="$t('components.location.create_modal.location_description')"
|
||||
:max-length="1000"
|
||||
/>
|
||||
<LocationSelector v-model="form.parent" />
|
||||
<div class="modal-action">
|
||||
<div class="flex justify-center">
|
||||
<BaseButton class="rounded-r-none" type="submit" :loading="loading">{{ $t("global.create") }}</BaseButton>
|
||||
@@ -59,10 +59,23 @@
|
||||
parent: null as LocationSummary | null,
|
||||
});
|
||||
|
||||
whenever(
|
||||
watch(
|
||||
() => modal.value,
|
||||
() => {
|
||||
open => {
|
||||
if (open) {
|
||||
useTimeoutFn(() => {
|
||||
focused.value = true;
|
||||
}, 50);
|
||||
|
||||
if (locationId.value) {
|
||||
const found = locations.value.find(l => l.id === locationId.value);
|
||||
if (found) {
|
||||
form.parent = found;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
focused.value = false;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -77,8 +90,20 @@
|
||||
const api = useUserApi();
|
||||
const toast = useNotifier();
|
||||
|
||||
const locationsStore = useLocationStore();
|
||||
const locations = computed(() => locationsStore.allLocations);
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { shift } = useMagicKeys();
|
||||
|
||||
const locationId = computed(() => {
|
||||
if (route.fullPath.includes("/location/")) {
|
||||
return route.params.id;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
async function create(close = true) {
|
||||
if (loading.value) {
|
||||
toast.error("Already creating a location");
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Combobox v-model="selectedAction">
|
||||
<Combobox v-model="selectedAction" :nullable="true">
|
||||
<ComboboxInput
|
||||
ref="inputBox"
|
||||
class="input input-bordered mt-2 w-full"
|
||||
@@ -7,6 +7,7 @@
|
||||
></ComboboxInput>
|
||||
<ComboboxOptions
|
||||
class="card dropdown-content absolute max-h-48 w-full overflow-y-scroll rounded-lg border border-base-300 bg-base-100"
|
||||
:unmount="false"
|
||||
>
|
||||
<ComboboxOption
|
||||
v-for="(action, idx) in filteredActions"
|
||||
@@ -68,21 +69,20 @@
|
||||
},
|
||||
});
|
||||
|
||||
const selectedAction = useVModel(props, "modelValue");
|
||||
const emit = defineEmits(["update:modelValue", "actionSelected"]);
|
||||
const selectedAction = ref(null);
|
||||
|
||||
const inputValue = ref("");
|
||||
const inputBox = ref();
|
||||
const inputBoxButton = ref();
|
||||
const { focused: inputBoxFocused } = useFocus(inputBox);
|
||||
|
||||
const emit = defineEmits(["update:modelValue", "quickSelect"]);
|
||||
|
||||
const revealActions = () => {
|
||||
unrefElement(inputBoxButton).click();
|
||||
};
|
||||
|
||||
watch(inputBoxFocused, () => {
|
||||
if (inputBoxFocused.value) revealActions();
|
||||
watch(inputBoxFocused, val => {
|
||||
if (val) revealActions();
|
||||
else inputValue.value = "";
|
||||
});
|
||||
|
||||
@@ -90,11 +90,19 @@
|
||||
if (!oldVal) {
|
||||
const action = props.actions?.find(v => v.shortcut === val);
|
||||
if (action) {
|
||||
emit("quickSelect", action);
|
||||
emit("actionSelected", action);
|
||||
inputBoxFocused.value = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
watch(selectedAction, val => {
|
||||
if (val) {
|
||||
emit("actionSelected", val);
|
||||
selectedAction.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
const filteredActions = computed(() => {
|
||||
const searchTerm = inputValue.value.toLowerCase();
|
||||
return (props.actions || []).filter(action => {
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<BaseModal v-model="modal" :show-close-button="false">
|
||||
<BaseModal
|
||||
v-model="modal"
|
||||
:show-close-button="false"
|
||||
:click-outside-to-close="true"
|
||||
:modal-top="true"
|
||||
:class="{ 'self-start': true }"
|
||||
>
|
||||
<div class="relative">
|
||||
<span class="text-neutral-400">{{ $t("components.quick_menu.shortcut_hint") }}</span>
|
||||
<QuickMenuInput
|
||||
ref="inputBox"
|
||||
v-model="selectedAction"
|
||||
:actions="props.actions || []"
|
||||
@quick-select="invokeAction"
|
||||
></QuickMenuInput>
|
||||
<QuickMenuInput ref="inputBox" :actions="props.actions || []" @action-selected="invokeAction"></QuickMenuInput>
|
||||
</div>
|
||||
</BaseModal>
|
||||
</template>
|
||||
@@ -28,7 +29,6 @@
|
||||
});
|
||||
|
||||
const modal = useVModel(props, "modelValue");
|
||||
const selectedAction = ref<QuickMenuAction>();
|
||||
|
||||
const inputBox = ref<QuickMenuInputData>({ focused: false, revealActions: () => {} });
|
||||
|
||||
@@ -37,7 +37,6 @@
|
||||
}, 50).start;
|
||||
|
||||
const onModalClose = () => {
|
||||
selectedAction.value = undefined;
|
||||
inputBox.value.focused = false;
|
||||
};
|
||||
|
||||
@@ -51,8 +50,4 @@
|
||||
modal.value = false;
|
||||
useTimeoutFn(action.action, 100).start();
|
||||
}
|
||||
|
||||
watch(selectedAction, action => {
|
||||
if (action) invokeAction(action);
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -35,7 +35,9 @@
|
||||
<p class="text-center text-sm">
|
||||
<a href="https://github.com/sysadminsmedia/homebox/releases/tag/{{ status.build.version }}" target="_blank">
|
||||
{{ $t("global.version", { version: status.build.version }) }} ~
|
||||
{{ $t("global.build", { build: status.build.commit }) }}</a> ~
|
||||
{{ $t("global.build", { build: status.build.commit }) }}</a
|
||||
>
|
||||
~
|
||||
<a href="https://homebox.software/en/api.html" target="_blank">API</a>
|
||||
</p>
|
||||
</footer>
|
||||
@@ -70,9 +72,11 @@
|
||||
<button class="group" @click="btn.action">
|
||||
{{ btn.name.value }}
|
||||
|
||||
<kbd v-if="btn.shortcut" class="ml-auto hidden text-neutral-400 group-hover:inline">{{
|
||||
btn.shortcut
|
||||
}}</kbd>
|
||||
<kbd
|
||||
v-if="btn.shortcut"
|
||||
class="kbd kbd-sm ml-auto hidden text-neutral-400 group-hover:inline"
|
||||
>{{ btn.shortcut.replaceAll("Shift+", "⇧") }}</kbd
|
||||
>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -145,9 +149,14 @@
|
||||
|
||||
const displayOutdatedWarning = computed(() => Boolean(!isDev.value && !hasHiddenLatest.value && isOutdated.value));
|
||||
|
||||
const activeElement = useActiveElement();
|
||||
const keys = useMagicKeys({
|
||||
aliasMap: {
|
||||
"⌃": "control_",
|
||||
"Shift+": "ShiftLeft_",
|
||||
"1": "digit1",
|
||||
"2": "digit2",
|
||||
"3": "digit3",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -173,7 +182,7 @@
|
||||
{
|
||||
id: 0,
|
||||
name: computed(() => t("menu.create_item")),
|
||||
shortcut: "⌃1",
|
||||
shortcut: "Shift+1",
|
||||
action: () => {
|
||||
modals.item = true;
|
||||
},
|
||||
@@ -181,7 +190,7 @@
|
||||
{
|
||||
id: 1,
|
||||
name: computed(() => t("menu.create_location")),
|
||||
shortcut: "⌃2",
|
||||
shortcut: "Shift+2",
|
||||
action: () => {
|
||||
modals.location = true;
|
||||
},
|
||||
@@ -189,7 +198,7 @@
|
||||
{
|
||||
id: 2,
|
||||
name: computed(() => t("menu.create_label")),
|
||||
shortcut: "⌃3",
|
||||
shortcut: "Shift+3",
|
||||
action: () => {
|
||||
modals.label = true;
|
||||
},
|
||||
@@ -197,9 +206,12 @@
|
||||
];
|
||||
|
||||
dropdown.forEach(option => {
|
||||
if (option.shortcut) {
|
||||
whenever(keys[option.shortcut], () => {
|
||||
if (option?.shortcut) {
|
||||
const shortcutKeycode = option.shortcut.replace(/([0-9])/, "digit$&");
|
||||
whenever(keys[shortcutKeycode], () => {
|
||||
if (activeElement.value?.tagName !== "INPUT") {
|
||||
option.action();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -267,7 +279,7 @@
|
||||
modals.import = false;
|
||||
});
|
||||
|
||||
const quickMenuActions = ref(
|
||||
const quickMenuActions = reactive(
|
||||
[
|
||||
{
|
||||
text: computed(() => `${t("global.create")}: ${t("menu.create_item")}`),
|
||||
|
||||
Reference in New Issue
Block a user