more form stuff, add storages
This commit is contained in:
parent
dfab6b8a18
commit
c76db68e5b
@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<button
|
<button
|
||||||
class="relative"
|
class="relative"
|
||||||
|
type="button"
|
||||||
@click="inputRef.click()"
|
@click="inputRef.click()"
|
||||||
:class="[
|
: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',
|
'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">
|
<script setup lang="ts">
|
||||||
import { useDebounceFn } from '@vueuse/shared';
|
import { useDebounceFn } from '@vueuse/shared';
|
||||||
|
import get from 'lodash.get';
|
||||||
import { provide, ref, watch } from 'vue';
|
import { provide, ref, watch } from 'vue';
|
||||||
import deepUnref from '../../utils/deep-unref';
|
import deepUnref from '../../utils/deep-unref';
|
||||||
import { FormData, FormErrors, FormSubmit } from './form.types';
|
import { FormData, FormErrors, FormSubmit } from './form.types';
|
||||||
@ -42,7 +43,11 @@ const validateField = async (field: string) => {
|
|||||||
for (const validator of props.validators) {
|
for (const validator of props.validators) {
|
||||||
if (validator.field !== field) continue;
|
if (validator.field !== field) continue;
|
||||||
for (const fn of validator.validators) {
|
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) {
|
if (!result.isValid) {
|
||||||
formErrors.value = {
|
formErrors.value = {
|
||||||
...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
|
>{{ label }}</label
|
||||||
>
|
>
|
||||||
<input
|
<slot
|
||||||
|
name="input"
|
||||||
|
:for-id="forId"
|
||||||
|
:type="type"
|
||||||
|
:id="forId"
|
||||||
|
:fieldName="name"
|
||||||
|
:value="value"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
:invalid="!!errors?.length"
|
||||||
|
:onInput="onInput"
|
||||||
|
:onChange="onChange"
|
||||||
|
:onFocus="onFocus"
|
||||||
|
:onBlur="onBlur"
|
||||||
|
:setValue="setValue"
|
||||||
|
>
|
||||||
|
<FormInput
|
||||||
|
:for-id="forId"
|
||||||
:type="type"
|
:type="type"
|
||||||
:id="forId"
|
:id="forId"
|
||||||
:name="name"
|
:name="name"
|
||||||
:value="value"
|
:value="value"
|
||||||
:class="inputClass"
|
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
@input="onInput($event)"
|
:invalid="!!errors?.length"
|
||||||
@change="onChange($event)"
|
@input="onInput"
|
||||||
|
@change="onChange"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
@blur="onBlur"
|
@blur="onBlur"
|
||||||
|
@set-value="setValue"
|
||||||
/>
|
/>
|
||||||
|
</slot>
|
||||||
<slot />
|
<slot />
|
||||||
<span
|
<span
|
||||||
class="text-sm text-red-500"
|
class="text-sm text-red-500"
|
||||||
@ -31,9 +49,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 { inject } from 'vue';
|
||||||
import { FormData, FormErrors } from './form.types';
|
import { FormData, FormErrors } from './form.types';
|
||||||
|
import FormInput from './FormInput.vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@ -62,42 +83,43 @@ const emit = defineEmits<{
|
|||||||
|
|
||||||
const formData = inject<Ref<FormData>>('formData');
|
const formData = inject<Ref<FormData>>('formData');
|
||||||
const formErrors = inject<Ref<FormErrors>>('formErrors');
|
const formErrors = inject<Ref<FormErrors>>('formErrors');
|
||||||
|
const formGroup = inject<Ref<string>>('formGroup', ref(''));
|
||||||
const fieldChange = inject<(field: string) => void>('fieldChange');
|
const fieldChange = inject<(field: string) => void>('fieldChange');
|
||||||
const forId = computed(() => `form-${props.name}`);
|
|
||||||
const value = computed(() => formData?.value[props.name]);
|
const fieldName = computed(() =>
|
||||||
const errors = computed(() => formErrors?.value[props.name] || []);
|
formGroup?.value ? `${formGroup.value}.${props.name}` : props.name
|
||||||
const inputClass = computed(() => {
|
);
|
||||||
return [
|
|
||||||
errors.value.length
|
const forId = computed(() => `form-${fieldName}`);
|
||||||
? 'border-red-300 focus:border-red-500 focus:ring-red-500'
|
const value = computed(() => get(formData?.value, fieldName.value));
|
||||||
: 'border-gray-300 focus:border-blue-500 focus:ring-blue-500',
|
const errors = computed(() => formErrors?.value[fieldName.value] || []);
|
||||||
`mt-1 block w-full rounded-md shadow-sm sm:text-sm transition-colors duration-200`,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
const onInput = (ev: Event) => {
|
const onInput = (ev: Event) => {
|
||||||
if (!formData) return;
|
if (!formData) return;
|
||||||
const value = (ev.target as HTMLInputElement).value;
|
const value = (ev.target as HTMLInputElement).value;
|
||||||
const cleanValue = props.type === 'number' ? parseFloat(value) : value;
|
const cleanValue = props.type === 'number' ? parseFloat(value) : value;
|
||||||
formData.value[props.name] = cleanValue;
|
setValue(cleanValue);
|
||||||
emit('change', cleanValue);
|
|
||||||
fieldChange?.(props.name);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChange = (ev: Event) => {
|
const onChange = (ev: Event) => {
|
||||||
if (!formData) return;
|
if (!formData) return;
|
||||||
if (props.type === 'checkbox' || props.type === 'radio') {
|
if (props.type === 'checkbox' || props.type === 'radio') {
|
||||||
const cleanValue = (ev.target as HTMLInputElement).checked;
|
const cleanValue = (ev.target as HTMLInputElement).checked;
|
||||||
formData.value[props.name] = cleanValue;
|
setValue(cleanValue);
|
||||||
emit('change', cleanValue);
|
|
||||||
fieldChange?.(props.name);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setValue = (value: unknown) => {
|
||||||
|
if (!formData) return;
|
||||||
|
set(formData.value, fieldName.value, value);
|
||||||
|
emit('change', value);
|
||||||
|
fieldChange?.(fieldName.value);
|
||||||
|
};
|
||||||
|
|
||||||
const onFocus = () => emit('focus');
|
const onFocus = () => emit('focus');
|
||||||
|
|
||||||
const onBlur = () => {
|
const onBlur = () => {
|
||||||
emit('blur');
|
emit('blur');
|
||||||
fieldChange?.(props.name);
|
fieldChange?.(fieldName.value);
|
||||||
};
|
};
|
||||||
</script>
|
</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 {
|
export function MaxLength(length: number, message?: string): FormValidatorFn {
|
||||||
return (_: string, value: unknown) => {
|
return (_: string, value: unknown) => {
|
||||||
let isValid = true;
|
let isValid = true;
|
||||||
if (value != null && typeof value === 'string' && value.length >= length)
|
if (value != null && typeof value === 'string' && value.length > length)
|
||||||
isValid = false;
|
isValid = false;
|
||||||
return {
|
return {
|
||||||
isValid,
|
isValid,
|
||||||
|
@ -5,9 +5,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #default="{ closeModal }">
|
<template #default="{ closeModal }">
|
||||||
|
<FormAlert :message="error" />
|
||||||
<Form @submit="onSubmit" v-model="data" :validators="validators">
|
<Form @submit="onSubmit" v-model="data" :validators="validators">
|
||||||
<FormField name="displayName" label="Display Name" />
|
<FormField name="displayName" label="Display Name" />
|
||||||
<FormField name="type" label="Type" />
|
<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>
|
<button type="submit">Submit</button>
|
||||||
</Form>
|
</Form>
|
||||||
</template>
|
</template>
|
||||||
@ -16,22 +21,37 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import { useAccessToken } from '../../composables/useAccessToken';
|
||||||
|
import { BACKEND_URL } from '../../constants';
|
||||||
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
import { BuildingListItem } from '../../interfaces/building.interfaces';
|
||||||
import { RoomListItem } from '../../interfaces/room.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 { FormSubmit } from '../form/form.types';
|
||||||
import Form from '../form/Form.vue';
|
import Form from '../form/Form.vue';
|
||||||
|
import FormAlert from '../form/FormAlert.vue';
|
||||||
import FormField from '../form/FormField.vue';
|
import FormField from '../form/FormField.vue';
|
||||||
import { IsRequired, MinLength } from '../form/validators';
|
import { IsRequired, MinLength } from '../form/validators';
|
||||||
import Modal from '../Modal.vue';
|
import Modal from '../Modal.vue';
|
||||||
|
|
||||||
|
const { authHeader } = useAccessToken();
|
||||||
|
const defaults = {
|
||||||
|
displayName: '',
|
||||||
|
type: '',
|
||||||
|
locationDescription: '',
|
||||||
|
color: '#000000',
|
||||||
|
};
|
||||||
|
|
||||||
const modalRef = ref<InstanceType<typeof Modal>>();
|
const modalRef = ref<InstanceType<typeof Modal>>();
|
||||||
const room = ref<RoomListItem>();
|
const room = ref<RoomListItem>();
|
||||||
const building = ref<BuildingListItem>();
|
const building = ref<BuildingListItem>();
|
||||||
const set = ref<StorageSetListItem | boolean>();
|
const set = ref<StorageSetListItem | boolean>();
|
||||||
const data = ref({
|
const data = ref({ ...defaults });
|
||||||
displayName: '',
|
const error = ref('');
|
||||||
});
|
|
||||||
|
|
||||||
const validators = ref([
|
const validators = ref([
|
||||||
{
|
{
|
||||||
@ -44,6 +64,10 @@ const validators = ref([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'added', storage: StorageSharedType): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
openModal: (
|
openModal: (
|
||||||
useBuilding: BuildingListItem,
|
useBuilding: BuildingListItem,
|
||||||
@ -53,9 +77,49 @@ defineExpose({
|
|||||||
room.value = useRoom;
|
room.value = useRoom;
|
||||||
building.value = useBuilding;
|
building.value = useBuilding;
|
||||||
set.value = useSet;
|
set.value = useSet;
|
||||||
|
data.value = { ...defaults };
|
||||||
modalRef.value?.openModal();
|
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>
|
</script>
|
||||||
|
@ -479,7 +479,7 @@ export class HousePlannerCanvas {
|
|||||||
|
|
||||||
private calculateViewport(box: Vec2Box): [number, Vec2] {
|
private calculateViewport(box: Vec2Box): [number, Vec2] {
|
||||||
let [min, max] = box;
|
let [min, max] = box;
|
||||||
const gap = this.headless ? 10 : 80;
|
const gap = this.headless ? 50 : 80;
|
||||||
min = vec2Sub(min, [gap, gap]);
|
min = vec2Sub(min, [gap, gap]);
|
||||||
max = vec2Add(max, [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-if="selectedRoom?.id === room.id">
|
||||||
<template v-for="storage of storages">
|
<template v-for="storage of storages">
|
||||||
<StorageBubble
|
<StorageBubble
|
||||||
v-if="!movingBubble || storage.id === movingBubble?.id"
|
v-if="
|
||||||
|
!movingBubble ||
|
||||||
|
(storage.id === movingBubble?.id &&
|
||||||
|
isSet(storage) === isSet(movingBubble))
|
||||||
|
"
|
||||||
:storage="storage"
|
:storage="storage"
|
||||||
:class="{
|
:class="{
|
||||||
'z-20':
|
'z-20':
|
||||||
@ -111,9 +115,9 @@
|
|||||||
:selected-room="selectedRoom"
|
:selected-room="selectedRoom"
|
||||||
:selected-storage="selectedStorage"
|
:selected-storage="selectedStorage"
|
||||||
:selected-set="selectedSet"
|
:selected-set="selectedSet"
|
||||||
@select-room="(room) => selectRoomFromList(room)"
|
@select-room="selectRoomFromList"
|
||||||
@select-storage="(storage) => selectStorage(storage)"
|
@select-storage="selectStorage"
|
||||||
@new-storage="(set) => addNewStorage(set)"
|
@new-storage="addNewStorage"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<StoredItemCard
|
<StoredItemCard
|
||||||
@ -122,7 +126,7 @@
|
|||||||
:stored-item="stored"
|
:stored-item="stored"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NewStorageModal ref="newStorage" />
|
<NewStorageModal ref="newStorage" @added="storageAdded" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
@ -186,6 +190,7 @@ const clickOnRoom = (room?: RoomLayoutObject, x?: number, y?: number) => {
|
|||||||
selectedStorage.value = undefined;
|
selectedStorage.value = undefined;
|
||||||
selectedSet.value = undefined;
|
selectedSet.value = undefined;
|
||||||
if (room && (!selectedRoom.value || room.id !== selectedRoom.value?.id)) {
|
if (room && (!selectedRoom.value || room.id !== selectedRoom.value?.id)) {
|
||||||
|
movingBubble.value = undefined;
|
||||||
getRoomStorages(room);
|
getRoomStorages(room);
|
||||||
} else if (room?.id == selectedRoom.value?.id) {
|
} else if (room?.id == selectedRoom.value?.id) {
|
||||||
if (movingBubble.value && x != null && y != null) {
|
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) => {
|
const mouseMovedInRoom = (room: RoomLayoutObject, x: number, y: number) => {
|
||||||
if (movingBubble.value) {
|
if (!movingBubble.value) return;
|
||||||
const storage = storages.value.find(
|
movingBubble.value.location = `${x},${y}`;
|
||||||
(item) => item.id === movingBubble.value?.id
|
|
||||||
);
|
|
||||||
if (!storage) return;
|
|
||||||
storage.location = `${x},${y}`;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const moveBubble = (bubble: StorageSharedType) => {
|
const moveBubble = (bubble: StorageSharedType) => {
|
||||||
@ -245,12 +245,17 @@ const selectRoomFromList = (room: RoomLayoutObject) => {
|
|||||||
selectedRoom.value = room;
|
selectedRoom.value = room;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectStorage = (storage: StorageSharedType) => {
|
const selectStorage = (
|
||||||
|
storage: StorageSharedType,
|
||||||
|
parentSet?: StorageSetListItem
|
||||||
|
) => {
|
||||||
if (isSet(storage)) {
|
if (isSet(storage)) {
|
||||||
selectedSet.value = storage as StorageSetListItem;
|
selectedSet.value = storage as StorageSetListItem;
|
||||||
|
selectedStorage.value = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedSet.value = parentSet;
|
||||||
selectedStorage.value = storage as StorageListItem;
|
selectedStorage.value = storage as StorageListItem;
|
||||||
jfetch(`${BACKEND_URL}/storage/storages/${storage.id}`, {
|
jfetch(`${BACKEND_URL}/storage/storages/${storage.id}`, {
|
||||||
headers: authHeader.value,
|
headers: authHeader.value,
|
||||||
@ -300,4 +305,18 @@ const getRoomStorages = async (room: RoomLayoutObject) => {
|
|||||||
console.error(e);
|
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>
|
</script>
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
<template>
|
<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">
|
<div class="flex flex-col">
|
||||||
<button
|
<button
|
||||||
v-for="room of rooms"
|
v-for="room of rooms"
|
||||||
@ -15,13 +20,18 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col" v-if="selectedRoom?.id">
|
<template v-if="selectedRoom?.id">
|
||||||
|
<div class="my-2 flex justify-center md:hidden">
|
||||||
|
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
<button
|
<button
|
||||||
v-for="storage of storages"
|
v-for="storage of storages"
|
||||||
@click="emit('selectStorage', storage)"
|
@click="emit('selectStorage', storage)"
|
||||||
:class="[
|
:class="[
|
||||||
(isSet(storage) && selectedSet?.id === storage.id) ||
|
(isSet(storage) && selectedSet?.id === storage.id) ||
|
||||||
selectedStorage?.id === storage.id
|
(selectedStorage?.id === storage.id && !isSet(storage))
|
||||||
? 'bg-blue-100 hover:bg-blue-200'
|
? 'bg-blue-100 hover:bg-blue-200'
|
||||||
: 'hover:bg-blue-100',
|
: 'hover:bg-blue-100',
|
||||||
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
||||||
@ -34,7 +44,9 @@
|
|||||||
>({{ (storage as StorageListItem).itemCount }})</span
|
>({{ (storage as StorageListItem).itemCount }})</span
|
||||||
>
|
>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
(set) ({{ (storage as StorageSetListItem).storages.length }})</span
|
(set) ({{
|
||||||
|
(storage as StorageSetListItem).storages.length
|
||||||
|
}})</span
|
||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
<ChevronRightIcon class="h-4 w-4" />
|
<ChevronRightIcon class="h-4 w-4" />
|
||||||
@ -56,13 +68,21 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col" v-if="selectedSet">
|
<template v-if="selectedSet">
|
||||||
|
<div class="my-2 flex justify-center md:hidden">
|
||||||
|
<ChevronDoubleDownIcon class="h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
<button
|
<button
|
||||||
v-for="storage of selectedSet.storages"
|
v-for="storage of selectedSet.storages"
|
||||||
@click="emit('selectStorage', storage)"
|
@click="emit('selectStorage', storage, selectedSet)"
|
||||||
:class="[
|
:class="[
|
||||||
'hover:bg-blue-100',
|
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',
|
'flex items-center justify-between border-b-2 border-gray-100 py-2 px-2',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
@ -79,8 +99,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div class="flex flex-col" v-if="selectedStorage?.items">
|
<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
|
<button
|
||||||
v-for="item of selectedStorage.items"
|
v-for="item of selectedStorage.items"
|
||||||
:class="[
|
:class="[
|
||||||
@ -101,11 +127,23 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 isSet from '../../../utils/is-storage-set';
|
||||||
import {
|
import {
|
||||||
StorageListItem,
|
StorageListItem,
|
||||||
@ -123,7 +161,11 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'selectStorage', storage: StorageSharedType): void;
|
(
|
||||||
|
e: 'selectStorage',
|
||||||
|
storage: StorageSharedType,
|
||||||
|
parentSet?: StorageSetListItem
|
||||||
|
): void;
|
||||||
(e: 'selectRoom', room: RoomLayoutObject): void;
|
(e: 'selectRoom', room: RoomLayoutObject): void;
|
||||||
(e: 'newStorage', set: boolean | StorageSetListItem): void;
|
(e: 'newStorage', set: boolean | StorageSetListItem): void;
|
||||||
(e: 'addItem', storage: StorageListItem): void;
|
(e: 'addItem', storage: StorageListItem): void;
|
||||||
|
@ -15,7 +15,9 @@
|
|||||||
<button
|
<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"
|
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')"
|
@click.stop="emit('startMoving')"
|
||||||
|
title="Move storage"
|
||||||
>
|
>
|
||||||
|
<span class="sr-only">Move storage</span>
|
||||||
<ArrowsPointingOutIcon class="h-4 w-4" />
|
<ArrowsPointingOutIcon class="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user