more form stuff, add storages
This commit is contained in:
parent
dfab6b8a18
commit
c76db68e5b
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
class="relative"
|
||||
type="button"
|
||||
@click="inputRef.click()"
|
||||
:class="[
|
||||
'flex items-center space-x-1 rounded-full bg-gray-100 py-1 hover:bg-gray-50 focus:ring-2 focus:ring-blue-200',
|
||||
|
@ -8,6 +8,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDebounceFn } from '@vueuse/shared';
|
||||
import get from 'lodash.get';
|
||||
import { provide, ref, watch } from 'vue';
|
||||
import deepUnref from '../../utils/deep-unref';
|
||||
import { FormData, FormErrors, FormSubmit } from './form.types';
|
||||
@ -42,7 +43,11 @@ const validateField = async (field: string) => {
|
||||
for (const validator of props.validators) {
|
||||
if (validator.field !== field) continue;
|
||||
for (const fn of validator.validators) {
|
||||
const result = await fn(field, formData.value[field], formData.value);
|
||||
const result = await fn(
|
||||
field,
|
||||
get(formData.value, field),
|
||||
formData.value
|
||||
);
|
||||
if (!result.isValid) {
|
||||
formErrors.value = {
|
||||
...formErrors.value,
|
||||
|
23
src/components/form/FormAlert.vue
Normal file
23
src/components/form/FormAlert.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition-height ease-out duration-500 origin-bottom overflow-hidden"
|
||||
enter-from-class="h-0"
|
||||
enter-to-class="h-10"
|
||||
>
|
||||
<div class="mb-4 rounded" v-if="message">
|
||||
<div
|
||||
class="rounded bg-red-100 px-4 py-2 text-center font-bold text-red-700"
|
||||
role="alert"
|
||||
aria-live="assertive"
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{
|
||||
message?: string;
|
||||
}>();
|
||||
</script>
|
@ -8,18 +8,36 @@
|
||||
]"
|
||||
>{{ label }}</label
|
||||
>
|
||||
<input
|
||||
<slot
|
||||
name="input"
|
||||
:for-id="forId"
|
||||
:type="type"
|
||||
:id="forId"
|
||||
:name="name"
|
||||
:fieldName="name"
|
||||
:value="value"
|
||||
:class="inputClass"
|
||||
:placeholder="placeholder"
|
||||
@input="onInput($event)"
|
||||
@change="onChange($event)"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
:invalid="!!errors?.length"
|
||||
:onInput="onInput"
|
||||
:onChange="onChange"
|
||||
:onFocus="onFocus"
|
||||
:onBlur="onBlur"
|
||||
:setValue="setValue"
|
||||
>
|
||||
<FormInput
|
||||
:for-id="forId"
|
||||
:type="type"
|
||||
:id="forId"
|
||||
:name="name"
|
||||
:value="value"
|
||||
:placeholder="placeholder"
|
||||
:invalid="!!errors?.length"
|
||||
@input="onInput"
|
||||
@change="onChange"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@set-value="setValue"
|
||||
/>
|
||||
</slot>
|
||||
<slot />
|
||||
<span
|
||||
class="text-sm text-red-500"
|
||||
@ -31,9 +49,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, Ref } from 'vue';
|
||||
import set from 'lodash.set';
|
||||
import get from 'lodash.get';
|
||||
import { computed, ref, Ref } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import { FormData, FormErrors } from './form.types';
|
||||
import FormInput from './FormInput.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@ -62,42 +83,43 @@ const emit = defineEmits<{
|
||||
|
||||
const formData = inject<Ref<FormData>>('formData');
|
||||
const formErrors = inject<Ref<FormErrors>>('formErrors');
|
||||
const formGroup = inject<Ref<string>>('formGroup', ref(''));
|
||||
const fieldChange = inject<(field: string) => void>('fieldChange');
|
||||
const forId = computed(() => `form-${props.name}`);
|
||||
const value = computed(() => formData?.value[props.name]);
|
||||
const errors = computed(() => formErrors?.value[props.name] || []);
|
||||
const inputClass = computed(() => {
|
||||
return [
|
||||
errors.value.length
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||
`mt-1 block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||
];
|
||||
});
|
||||
|
||||
const fieldName = computed(() =>
|
||||
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
|
||||
);
|
||||
|
||||
const forId = computed(() => `form-${fieldName}`);
|
||||
const value = computed(() => get(formData?.value, fieldName.value));
|
||||
const errors = computed(() => formErrors?.value[fieldName.value] || []);
|
||||
|
||||
const onInput = (ev: Event) => {
|
||||
if (!formData) return;
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
const cleanValue = props.type === 'number' ? parseFloat(value) : value;
|
||||
formData.value[props.name] = cleanValue;
|
||||
emit('change', cleanValue);
|
||||
fieldChange?.(props.name);
|
||||
setValue(cleanValue);
|
||||
};
|
||||
|
||||
const onChange = (ev: Event) => {
|
||||
if (!formData) return;
|
||||
if (props.type === 'checkbox' || props.type === 'radio') {
|
||||
const cleanValue = (ev.target as HTMLInputElement).checked;
|
||||
formData.value[props.name] = cleanValue;
|
||||
emit('change', cleanValue);
|
||||
fieldChange?.(props.name);
|
||||
setValue(cleanValue);
|
||||
}
|
||||
};
|
||||
|
||||
const setValue = (value: unknown) => {
|
||||
if (!formData) return;
|
||||
set(formData.value, fieldName.value, value);
|
||||
emit('change', value);
|
||||
fieldChange?.(fieldName.value);
|
||||
};
|
||||
|
||||
const onFocus = () => emit('focus');
|
||||
|
||||
const onBlur = () => {
|
||||
emit('blur');
|
||||
fieldChange?.(props.name);
|
||||
fieldChange?.(fieldName.value);
|
||||
};
|
||||
</script>
|
||||
|
20
src/components/form/FormGroup.vue
Normal file
20
src/components/form/FormGroup.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<slot />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, provide, Ref, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
name: string;
|
||||
}>();
|
||||
|
||||
const name = ref(props.name);
|
||||
const prevGroup = inject<Ref<string>>('formGroup', ref(''));
|
||||
|
||||
if (prevGroup?.value) {
|
||||
name.value = `${prevGroup.value}.${props.name}`;
|
||||
}
|
||||
|
||||
provide('formGroup', name);
|
||||
</script>
|
65
src/components/form/FormInput.vue
Normal file
65
src/components/form/FormInput.vue
Normal file
@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<ColorInput
|
||||
v-if="type === 'color'"
|
||||
:for-id="forId"
|
||||
:model-value="(value as string)"
|
||||
@update:model-value="(newValue: string) => emit('setValue', newValue)"
|
||||
/>
|
||||
<input
|
||||
v-else
|
||||
:type="type"
|
||||
:id="forId"
|
||||
:name="name"
|
||||
:value="value"
|
||||
:class="inputClass"
|
||||
:placeholder="placeholder"
|
||||
@input="emit('input', $event)"
|
||||
@change="emit('change', $event)"
|
||||
@focus="emit('focus', $event)"
|
||||
@blur="emit('blur', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import ColorInput from '../ColorInput.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
type?:
|
||||
| 'text'
|
||||
| 'number'
|
||||
| 'password'
|
||||
| 'color'
|
||||
| 'email'
|
||||
| 'checkbox'
|
||||
| 'radio';
|
||||
forId: string;
|
||||
name: string;
|
||||
value: unknown;
|
||||
invalid?: boolean;
|
||||
placeholder?: string;
|
||||
}>(),
|
||||
{
|
||||
type: 'text',
|
||||
invalid: false,
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'setValue', value: unknown): void;
|
||||
(e: 'input', ev: Event): void;
|
||||
(e: 'change', ev: Event): void;
|
||||
(e: 'focus', ev: Event): void;
|
||||
(e: 'blur', ev: Event): void;
|
||||
}>();
|
||||
|
||||
const inputClass = computed(() => {
|
||||
return [
|
||||
props.invalid
|
||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
||||
`mt-1 block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
||||
];
|
||||
});
|
||||
</script>
|
@ -15,7 +15,7 @@ export function MinLength(length: number, message?: string): FormValidatorFn {
|
||||
export function MaxLength(length: number, message?: string): FormValidatorFn {
|
||||
return (_: string, value: unknown) => {
|
||||
let isValid = true;
|
||||
if (value != null && typeof value === 'string' && value.length >= length)
|
||||
if (value != null && typeof value === 'string' && value.length > length)
|
||||
isValid = false;
|
||||
return {
|
||||
isValid,
|
||||
|
@ -5,9 +5,14 @@
|
||||
</template>
|
||||
|
||||
<template #default="{ closeModal }">
|
||||
<FormAlert :message="error" />
|
||||
<Form @submit="onSubmit" v-model="data" :validators="validators">
|
||||
<FormField name="displayName" label="Display Name" />
|
||||
<FormField name="type" label="Type" />
|
||||
<FormField name="locationDescription" label="Location description">
|
||||
<span class="text-sm">Describe the location of this storage</span>
|
||||
</FormField>
|
||||
<FormField type="color" name="color" label="Color" />
|
||||
<button type="submit">Submit</button>
|
||||
</Form>
|
||||
</template>
|
||||
@ -16,22 +21,37 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useAccessToken } from '../../composables/useAccessToken';
|
||||
import { BACKEND_URL } from '../../constants';
|
||||
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
||||
import { RoomListItem } from '../../interfaces/room.interfaces';
|
||||
import { StorageSetListItem } from '../../interfaces/storage.interfaces';
|
||||
import {
|
||||
StorageSetListItem,
|
||||
StorageSharedType,
|
||||
} from '../../interfaces/storage.interfaces';
|
||||
import jfetch from '../../utils/jfetch';
|
||||
import takeError from '../../utils/take-error';
|
||||
import { FormSubmit } from '../form/form.types';
|
||||
import Form from '../form/Form.vue';
|
||||
import FormAlert from '../form/FormAlert.vue';
|
||||
import FormField from '../form/FormField.vue';
|
||||
import { IsRequired, MinLength } from '../form/validators';
|
||||
import Modal from '../Modal.vue';
|
||||
|
||||
const { authHeader } = useAccessToken();
|
||||
const defaults = {
|
||||
displayName: '',
|
||||
type: '',
|
||||
locationDescription: '',
|
||||
color: '#000000',
|
||||
};
|
||||
|
||||
const modalRef = ref<InstanceType<typeof Modal>>();
|
||||
const room = ref<RoomListItem>();
|
||||
const building = ref<BuildingListItem>();
|
||||
const set = ref<StorageSetListItem | boolean>();
|
||||
const data = ref({
|
||||
displayName: '',
|
||||
});
|
||||
const data = ref({ ...defaults });
|
||||
const error = ref('');
|
||||
|
||||
const validators = ref([
|
||||
{
|
||||
@ -44,6 +64,10 @@ const validators = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'added', storage: StorageSharedType): void;
|
||||
}>();
|
||||
|
||||
defineExpose({
|
||||
openModal: (
|
||||
useBuilding: BuildingListItem,
|
||||
@ -53,9 +77,49 @@ defineExpose({
|
||||
room.value = useRoom;
|
||||
building.value = useBuilding;
|
||||
set.value = useSet;
|
||||
data.value = { ...defaults };
|
||||
modalRef.value?.openModal();
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (value: FormSubmit) => console.log(value);
|
||||
const onSubmit = async (value: FormSubmit) => {
|
||||
error.value = '';
|
||||
|
||||
if (!value.isValid || !room.value) return;
|
||||
|
||||
const createURL = `${BACKEND_URL}/storage${
|
||||
set.value === true ? '/set' : ''
|
||||
}/room/${room.value.id}`;
|
||||
|
||||
let createdStorage: StorageSharedType;
|
||||
try {
|
||||
const response = await jfetch(createURL, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
...value.formData,
|
||||
location: '0,0',
|
||||
},
|
||||
headers: authHeader.value,
|
||||
});
|
||||
|
||||
createdStorage = response.data;
|
||||
} catch (e) {
|
||||
error.value = takeError(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add created storage to set
|
||||
if (set.value && typeof set.value !== 'boolean') {
|
||||
await jfetch(
|
||||
`${BACKEND_URL}/storage/set/${set.value.id}/${createdStorage.id}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: authHeader.value,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
modalRef.value?.closeModal();
|
||||
emit('added', createdStorage);
|
||||
};
|
||||
</script>
|
||||
|
@ -479,7 +479,7 @@ export class HousePlannerCanvas {
|
||||
|
||||
private calculateViewport(box: Vec2Box): [number, Vec2] {
|
||||
let [min, max] = box;
|
||||
const gap = this.headless ? 10 : 80;
|
||||
const gap = this.headless ? 50 : 80;
|
||||
min = vec2Sub(min, [gap, gap]);
|
||||
max = vec2Add(max, [gap, gap]);
|
||||
|
||||
|
15
src/utils/take-error.ts
Normal file
15
src/utils/take-error.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { JFetchError } from './jfetch';
|
||||
|
||||
export default function takeError(thrown: unknown): string {
|
||||
if ((thrown as JFetchError).data) {
|
||||
const message = (thrown as JFetchError).data.message;
|
||||
if (message) {
|
||||
if (Array.isArray(message)) {
|
||||
return message.join('\n');
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
if ((thrown as Error).message) return (thrown as Error).message;
|
||||
return 'An unexpected error occured';
|
||||
}
|
@ -67,7 +67,11 @@
|
||||
<template v-if="selectedRoom?.id === room.id">
|
||||
<template v-for="storage of storages">
|
||||
<StorageBubble
|
||||
v-if="!movingBubble || storage.id === movingBubble?.id"
|
||||
v-if="
|
||||
!movingBubble ||
|
||||
(storage.id === movingBubble?.id &&
|
||||
isSet(storage) === isSet(movingBubble))
|
||||
"
|
||||
:storage="storage"
|
||||
:class="{
|
||||
'z-20':
|
||||
@ -111,9 +115,9 @@
|
||||
:selected-room="selectedRoom"
|
||||
:selected-storage="selectedStorage"
|
||||
:selected-set="selectedSet"
|
||||
@select-room="(room) => selectRoomFromList(room)"
|
||||
@select-storage="(storage) => selectStorage(storage)"
|
||||
@new-storage="(set) => addNewStorage(set)"
|
||||
@select-room="selectRoomFromList"
|
||||
@select-storage="selectStorage"
|
||||
@new-storage="addNewStorage"
|
||||
/>
|
||||
|
||||
<StoredItemCard
|
||||
@ -122,7 +126,7 @@
|
||||
:stored-item="stored"
|
||||
/>
|
||||
|
||||
<NewStorageModal ref="newStorage" />
|
||||
<NewStorageModal ref="newStorage" @added="storageAdded" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -186,6 +190,7 @@ const clickOnRoom = (room?: RoomLayoutObject, x?: number, y?: number) => {
|
||||
selectedStorage.value = undefined;
|
||||
selectedSet.value = undefined;
|
||||
if (room && (!selectedRoom.value || room.id !== selectedRoom.value?.id)) {
|
||||
movingBubble.value = undefined;
|
||||
getRoomStorages(room);
|
||||
} else if (room?.id == selectedRoom.value?.id) {
|
||||
if (movingBubble.value && x != null && y != null) {
|
||||
@ -198,13 +203,8 @@ const clickOnRoom = (room?: RoomLayoutObject, x?: number, y?: number) => {
|
||||
};
|
||||
|
||||
const mouseMovedInRoom = (room: RoomLayoutObject, x: number, y: number) => {
|
||||
if (movingBubble.value) {
|
||||
const storage = storages.value.find(
|
||||
(item) => item.id === movingBubble.value?.id
|
||||
);
|
||||
if (!storage) return;
|
||||
storage.location = `${x},${y}`;
|
||||
}
|
||||
if (!movingBubble.value) return;
|
||||
movingBubble.value.location = `${x},${y}`;
|
||||
};
|
||||
|
||||
const moveBubble = (bubble: StorageSharedType) => {
|
||||
@ -245,12 +245,17 @@ const selectRoomFromList = (room: RoomLayoutObject) => {
|
||||
selectedRoom.value = room;
|
||||
};
|
||||
|
||||
const selectStorage = (storage: StorageSharedType) => {
|
||||
const selectStorage = (
|
||||
storage: StorageSharedType,
|
||||
parentSet?: StorageSetListItem
|
||||
) => {
|
||||
if (isSet(storage)) {
|
||||
selectedSet.value = storage as StorageSetListItem;
|
||||
selectedStorage.value = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
selectedSet.value = parentSet;
|
||||
selectedStorage.value = storage as StorageListItem;
|
||||
jfetch(`${BACKEND_URL}/storage/storages/${storage.id}`, {
|
||||
headers: authHeader.value,
|
||||
@ -300,4 +305,18 @@ const getRoomStorages = async (room: RoomLayoutObject) => {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
const storageAdded = async (newStorage: StorageSharedType) => {
|
||||
if (!selectedRoom.value) return;
|
||||
await getRoomStorages(selectedRoom.value);
|
||||
if (selectedSet.value) {
|
||||
selectedSet.value.storages = [
|
||||
...selectedSet.value.storages,
|
||||
{
|
||||
...(newStorage as StorageListItem),
|
||||
itemCount: 0,
|
||||
},
|
||||
];
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<div :class="['grid', selectedSet ? 'grid-cols-4' : 'grid-cols-3']">
|
||||
<div
|
||||
:class="[
|
||||
'flex flex-col md:grid',
|
||||
selectedSet ? 'md:grid-cols-4' : 'md:grid-cols-3',
|
||||
]"
|
||||
>
|
||||
<div class="flex flex-col">
|
||||
<button
|
||||
v-for="room of rooms"
|
||||
@ -15,97 +20,130 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col" v-if="selectedRoom?.id">
|
||||
<button
|
||||
v-for="storage of storages"
|
||||
@click="emit('selectStorage', storage)"
|
||||
:class="[
|
||||
(isSet(storage) && selectedSet?.id === storage.id) ||
|
||||
selectedStorage?.id === storage.id
|
||||
? 'bg-blue-100 hover:bg-blue-200'
|
||||
: 'hover:bg-blue-100',
|
||||
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
>{{ storage.displayName }}
|
||||
<template v-if="selectedRoom?.id">
|
||||
<div class="my-2 flex justify-center md:hidden">
|
||||
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<span v-if="!isSet(storage)"
|
||||
>({{ (storage as StorageListItem).itemCount }})</span
|
||||
<div class="flex flex-col">
|
||||
<button
|
||||
v-for="storage of storages"
|
||||
@click="emit('selectStorage', storage)"
|
||||
:class="[
|
||||
(isSet(storage) && selectedSet?.id === storage.id) ||
|
||||
(selectedStorage?.id === storage.id && !isSet(storage))
|
||||
? 'bg-blue-100 hover:bg-blue-200'
|
||||
: 'hover:bg-blue-100',
|
||||
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
||||
]"
|
||||
>
|
||||
<span
|
||||
>{{ storage.displayName }}
|
||||
|
||||
<span v-if="!isSet(storage)"
|
||||
>({{ (storage as StorageListItem).itemCount }})</span
|
||||
>
|
||||
<span v-else>
|
||||
(set) ({{
|
||||
(storage as StorageSetListItem).storages.length
|
||||
}})</span
|
||||
>
|
||||
</span>
|
||||
<ChevronRightIcon class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div class="mx-auto flex space-x-2 py-2">
|
||||
<button
|
||||
@click="emit('newStorage', false)"
|
||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
<span v-else>
|
||||
(set) ({{ (storage as StorageSetListItem).storages.length }})</span
|
||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span>
|
||||
</button>
|
||||
<span>·</span>
|
||||
<button
|
||||
@click="emit('newStorage', true)"
|
||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
</span>
|
||||
<ChevronRightIcon class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div class="mx-auto flex space-x-2 py-2">
|
||||
<button
|
||||
@click="emit('newStorage', false)"
|
||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span>
|
||||
</button>
|
||||
<span>·</span>
|
||||
<button
|
||||
@click="emit('newStorage', true)"
|
||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage Set</span>
|
||||
</button>
|
||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage Set</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex flex-col" v-if="selectedSet">
|
||||
<button
|
||||
v-for="storage of selectedSet.storages"
|
||||
@click="emit('selectStorage', storage)"
|
||||
:class="[
|
||||
'hover:bg-blue-100',
|
||||
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
||||
]"
|
||||
>
|
||||
<span>{{ storage.displayName }} ({{ storage.itemCount }})</span>
|
||||
<ChevronRightIcon class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div class="mx-auto flex space-x-2 py-2">
|
||||
<button
|
||||
@click="emit('newStorage', selectedSet!)"
|
||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span>
|
||||
</button>
|
||||
<template v-if="selectedSet">
|
||||
<div class="my-2 flex justify-center md:hidden">
|
||||
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col" v-if="selectedStorage?.items">
|
||||
<button
|
||||
v-for="item of selectedStorage.items"
|
||||
:class="[
|
||||
'hover:bg-blue-100',
|
||||
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
||||
]"
|
||||
>
|
||||
<span>{{ item.item.displayName }}</span>
|
||||
<ChevronRightIcon class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div class="mx-auto flex space-x-2 py-2">
|
||||
<div class="flex flex-col">
|
||||
<button
|
||||
@click="emit('addItem', selectedStorage!)"
|
||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
v-for="storage of selectedSet.storages"
|
||||
@click="emit('selectStorage', storage, selectedSet)"
|
||||
:class="[
|
||||
selectedStorage?.id === storage.id && !isSet(storage)
|
||||
? 'bg-blue-100 hover:bg-blue-200'
|
||||
: 'hover:bg-blue-100',
|
||||
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
||||
]"
|
||||
>
|
||||
<PlusIcon class="h-4 w-4" /> <span>Add Items</span>
|
||||
<span>{{ storage.displayName }} ({{ storage.itemCount }})</span>
|
||||
<ChevronRightIcon class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div class="mx-auto flex space-x-2 py-2">
|
||||
<button
|
||||
@click="emit('newStorage', selectedSet!)"
|
||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
<PlusIcon class="h-4 w-4" /> <span>Add Storage</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="selectedStorage?.items">
|
||||
<div class="my-2 flex justify-center md:hidden">
|
||||
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<button
|
||||
v-for="item of selectedStorage.items"
|
||||
:class="[
|
||||
'hover:bg-blue-100',
|
||||
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
||||
]"
|
||||
>
|
||||
<span>{{ item.item.displayName }}</span>
|
||||
<ChevronRightIcon class="h-4 w-4" />
|
||||
</button>
|
||||
|
||||
<div class="mx-auto flex space-x-2 py-2">
|
||||
<button
|
||||
@click="emit('addItem', selectedStorage!)"
|
||||
class="flex items-center space-x-1 text-blue-500 hover:text-blue-600 hover:underline"
|
||||
>
|
||||
<PlusIcon class="h-4 w-4" /> <span>Add Items</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div
|
||||
class="my-2 mb-4 flex justify-center md:hidden"
|
||||
v-if="selectedStorage?.items?.length"
|
||||
>
|
||||
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChevronRightIcon, PlusIcon } from '@heroicons/vue/24/outline';
|
||||
import {
|
||||
ChevronDoubleDownIcon,
|
||||
ChevronRightIcon,
|
||||
PlusIcon,
|
||||
} from '@heroicons/vue/24/outline';
|
||||
import isSet from '../../../utils/is-storage-set';
|
||||
import {
|
||||
StorageListItem,
|
||||
@ -123,7 +161,11 @@ const props = defineProps<{
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'selectStorage', storage: StorageSharedType): void;
|
||||
(
|
||||
e: 'selectStorage',
|
||||
storage: StorageSharedType,
|
||||
parentSet?: StorageSetListItem
|
||||
): void;
|
||||
(e: 'selectRoom', room: RoomLayoutObject): void;
|
||||
(e: 'newStorage', set: boolean | StorageSetListItem): void;
|
||||
(e: 'addItem', storage: StorageListItem): void;
|
||||
|
@ -15,7 +15,9 @@
|
||||
<button
|
||||
class="absolute left-1/2 bottom-0.5 -translate-x-1/2 rounded-full bg-gray-50 px-1 py-1 ring-1 ring-black ring-opacity-5"
|
||||
@click.stop="emit('startMoving')"
|
||||
title="Move storage"
|
||||
>
|
||||
<span class="sr-only">Move storage</span>
|
||||
<ArrowsPointingOutIcon class="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user