Upravy UI, refaktoring, davkove upravy a mazani fotek

This commit is contained in:
Michal Kváček
2023-08-28 22:02:24 +02:00
parent dcc3230cbe
commit dd248d4432
53 changed files with 636 additions and 357 deletions
+1 -10
View File
@@ -7,11 +7,6 @@ const props = defineProps({
modelValue: { modelValue: {
type: Object, type: Object,
}, },
resetAfterSubmit: {
type: Boolean,
required: false,
default: false
},
isRequestPending, isRequestPending,
saveButton: { saveButton: {
type: [Object, Boolean], type: [Object, Boolean],
@@ -27,10 +22,6 @@ const props = defineProps({
const emits = defineEmits(["save"]); const emits = defineEmits(["save"]);
const saveForm = () => { const saveForm = () => {
emits("save", props.modelValue); emits("save", props.modelValue);
if (props.resetAfterSubmit) {
console.error("Not implemented")
}
} }
const $q = useQuasar(); const $q = useQuasar();
@@ -47,7 +38,7 @@ const fullwidthBtn = computed(() => $q.screen.width <= $q.screen.sizes.md);
color="primary" color="primary"
type="submit" type="submit"
size="lg" size="lg"
:class="{'q-my-lg': true, 'q-mx-lg': !fullwidthBtn, 'btn--fullwidth': fullwidthBtn}" :class="{'q-my-lg': true, 'btn--fullwidth': fullwidthBtn}"
v-if="saveButton" v-if="saveButton"
:icon="props.saveButton.icon" :icon="props.saveButton.icon"
:loading="props.isRequestPending" :loading="props.isRequestPending"
@@ -0,0 +1,59 @@
<script setup>
import {aircraft} from "src/composables/objects/aircraft";
import {confirmObject} from "src/composables/dialog";
import {useQuasar} from "quasar";
const props = defineProps({
aircraft: {
type: Object,
required: true
}
});
const emits = defineEmits(["delete"]);
const $q = useQuasar();
const confirmDelete = (aircraftId) => {
$q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
.onOk(() => {
emits("delete", aircraftId);
});
};
</script>
<template>
<q-card class="q-mx-md q-my-md flight-detail-card">
<q-img :src="aircraft.photoUrl" v-if="aircraft.photoUrl"/>
<q-card-actions class="q-my-md">
<div class="col-grow">
<q-btn
flat
:to='{name: "aircraftEdit", params: {id: aircraft.id}}'
icon="edit"
text-color="primary"
label="Upravit"
/>
</div>
<q-btn @click="confirmDelete(aircraft.id)" icon="delete" flat text-color="red" label="Smazat"/>
</q-card-actions>
<q-card-section>
<q-list>
<q-item v-if="aircraft.manufacturer">
Výrobce: {{ aircraft.manufacturer }}
</q-item>
<q-item v-if="aircraft.model">
Model: {{ aircraft.model }}
</q-item>
<q-item v-if="aircraft.seats">
Počet míst: {{ aircraft.seats }}
</q-item>
</q-list>
</q-card-section>
<q-card-section>
<article v-html="aircraft.description" class="q-my-md"/>
</q-card-section>
</q-card>
</template>
@@ -1,5 +1,23 @@
<script setup></script> <script setup>
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
import {randomInt} from "src/composables/random";
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
</script>
<template> <template>
<q-skeleton></q-skeleton> <q-card class="flight-detail-card">
<q-skeleton type="rect" height="400px"/>
<q-card-section>
<q-skeleton type="text" v-for="i in 3" :key="i" :width="randomInt(30, 50)+'%'"/>
</q-card-section>
<q-card-section class="q-my-md">
<BasicTextSkeleton :paragraphs="1" :lines="randomInt(1, 3)"/>
</q-card-section>
</q-card>
<q-card class="flight-detail-card" flat>
<BasicListSkeleton :items-count-min="1" :items-count-max="4"/>
</q-card>
</template> </template>
@@ -1,5 +1,22 @@
<script setup></script> <script setup>
import {randomInt} from "src/composables/random";
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
</script>
<template> <template>
<em>Aircraft edit skeleton</em> <q-card class="flight-detail-card">
<q-skeleton type="rect" height="400px"/>
<q-card-section>
<q-skeleton type="QInput" v-for="i in 3" :key="i" class="q-my-lg" />
<q-skeleton type="QInput" height="200px" class="q-my-lg" />
</q-card-section>
<q-card-actions>
<!-- TODO: posunout vpravo, zvetsit a na mobilu udelat fullwidth-->
<q-skeleton type="QBtn" />
</q-card-actions>
</q-card>
</template> </template>
@@ -1,5 +1,9 @@
<script setup></script> <script setup>
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
</script>
<template> <template>
<em>Copilot skeleton</em> <q-card class="flight-detail-card">
<BasicListSkeleton :items-count-min="1" :items-count-max="5"/>
</q-card>
</template> </template>
+15 -3
View File
@@ -4,17 +4,24 @@ import {onMounted, reactive} from "vue";
import {setDefaultData} from "src/composables/form/formHandling"; import {setDefaultData} from "src/composables/form/formHandling";
import FormWrapper from "components/FormWrapper.vue"; import FormWrapper from "components/FormWrapper.vue";
import {commonProps} from "src/composables/inputs"; import {commonProps} from "src/composables/inputs";
import {isRequestPending} from "src/composables/form/props";
import {isRequired} from "src/composables/form/rules";
const props = defineProps({ const props = defineProps({
copilot: { copilot: {
type: Object type: Object
} },
isRequestPending
}) })
const formData = reactive({ const formData = reactive({
name: null, name: null,
}); });
const rules = {
name: [isRequired]
}
onMounted(() => { onMounted(() => {
if (props.copilot) { if (props.copilot) {
setDefaultData(formData, props.copilot); setDefaultData(formData, props.copilot);
@@ -24,7 +31,12 @@ onMounted(() => {
</script> </script>
<template> <template>
<FormWrapper v-model="formData"> <FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
<q-input v-bind="commonProps" label="Jméno" v-model="formData.name" /> <q-input
v-bind="commonProps"
label="Jméno"
:rules="rules.name"
v-model="formData.name"
/>
</FormWrapper> </FormWrapper>
</template> </template>
@@ -16,7 +16,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
<q-item> <q-item>
<q-list> <q-list>
<ListItemSkeleton v-for="i in randomInt(2, 5)" :key="i"/> <ListItemSkeleton v-for="i in randomInt(1, 3)" :key="i"/>
</q-list> </q-list>
</q-item> </q-item>
@@ -26,7 +26,7 @@ import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
</q-list> </q-list>
<article class="q-my-lg"> <article class="q-my-lg">
<BasicTextSkeleton :lines="randomInt(5, 10)" :paragraphs="randomInt(1, 3)" /> <BasicTextSkeleton :lines="randomInt(2, 5)" :paragraphs="randomInt(1, 2)" />
</article> </article>
<q-skeleton height="400px" square class="q-mt-lg"/> <q-skeleton height="400px" square class="q-mt-lg"/>
+15 -2
View File
@@ -1,7 +1,20 @@
<script setup> <script setup>
import {randomInt} from "src/composables/random";
import BasicTextSkeleton from "components/basicSkeletons/BasicTextSkeleton.vue";
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
</script> </script>
<template> <template>
<em>Flight edit skeleton</em> <q-card class="flight-detail-card">
</template> <q-skeleton type="rect" height="400px"/>
<q-card-section>
<q-skeleton type="QInput" v-for="i in 3" :key="i" />
</q-card-section>
</q-card>
<q-card class="flight-detail-card" flat>
<BasicListSkeleton :items-count-min="1" :items-count-max="4"/>
</q-card></template>
+1 -1
View File
@@ -33,7 +33,7 @@ const pointsOfInterests = computed(() => {
} }
path.push({ path.push({
label: flight.value.takeoffAirport.name, label: flight.value.landingAirport.name,
position: { position: {
lat: flight.value.landingAirport.gpsLatitude, lat: flight.value.landingAirport.gpsLatitude,
lng: flight.value.landingAirport.gpsLongitude lng: flight.value.landingAirport.gpsLongitude
@@ -6,6 +6,7 @@ import DatetimePicker from "components/inputs/DatetimePicker.vue";
import ComboboxInput from "components/inputs/ComboboxInput.vue"; import ComboboxInput from "components/inputs/ComboboxInput.vue";
import {commonProps} from "src/composables/inputs"; import {commonProps} from "src/composables/inputs";
import {isRequired} from "src/composables/form/rules"; import {isRequired} from "src/composables/form/rules";
import {isRequestPending} from "src/composables/form/props";
const props = defineProps({ const props = defineProps({
aircrafts: { aircrafts: {
@@ -19,9 +20,9 @@ const props = defineProps({
flight: { flight: {
type: Object, type: Object,
required: false required: false
} },
} isRequestPending,
) });
const formData = reactive({ const formData = reactive({
aircraft: null, aircraft: null,
@@ -69,7 +70,7 @@ watch(() => formData.takeoffAirport, () => {
</script> </script>
<template> <template>
<FormWrapper v-model="formData"> <FormWrapper v-model="formData" :is-request-pending="props.isRequestPending">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<q-card class="q-mx-md q-my-md"> <q-card class="q-mx-md q-my-md">
@@ -140,8 +141,6 @@ watch(() => formData.takeoffAirport, () => {
/> />
</q-card-section> </q-card-section>
</q-card> </q-card>
</div> </div>
</div> </div>
</FormWrapper> </FormWrapper>
@@ -25,6 +25,7 @@ const formData = reactive({
icon: "upload", icon: "upload",
align: "left" align: "left"
}'> }'>
<q-file <q-file
class="q-my-lg" class="q-my-lg"
outlined outlined
@@ -0,0 +1,57 @@
<script setup>
import {computed, ref} from "vue";
import {setDefaultData} from "src/composables/form/formHandling";
import DialogWrapper from "components/DialogWrapper.vue";
import FormWrapper from "components/FormWrapper.vue";
import {commonProps} from "src/composables/inputs";
import ComboboxInput from "components/inputs/ComboboxInput.vue";
const props = defineProps({
photos: {
type: Array
},
locations: {
type: Array
}
});
const formData = ref({
name: "",
description: "",
pointOfInterest: null,
photoIds: []
});
const setup = () => {
formData.value.photoIds = props.photos.map(p => p.id);
formData.value.name = "";
formData.value.description = "";
formData.value.pointOfInterest = null;
}
</script>
<template>
<DialogWrapper :formData="formData" @show="setup()">
<q-card-section class="text-center">
<!-- <q-scroll-area style="min-height: 80px; max-height: 150px">-->
<q-img :src="photo.thumbnailUrl" width="80px" class="q-ma-sm" v-for="photo in photos" :key="photo.thumbnailUrl"/>
<!-- {{ photos }}-->
<!-- </q-scroll-area>-->
</q-card-section>
<q-card-section>
<FormWrapper :save-button="false">
<q-input v-model="formData.name" label="Název" v-bind="commonProps"/>
<q-input type="textarea" v-model="formData.description" label="Popis" v-bind="commonProps"/>
<ComboboxInput
:options="locations"
v-bind="commonProps"
v-model="formData.pointOfInterest"
:enable-new-values="false"
label="POI"
/>
</FormWrapper>
</q-card-section>
</DialogWrapper>
</template>
+44 -8
View File
@@ -3,6 +3,7 @@ import EmbeddedMap from "components/map/EmbeddedMap.vue";
import {computed, onMounted, ref} from "vue"; import {computed, onMounted, ref} from "vue";
import {formatDatetime} from "src/composables/datetimeUtils"; import {formatDatetime} from "src/composables/datetimeUtils";
import 'vue-inner-image-zoom/lib/vue-inner-image-zoom.css' import 'vue-inner-image-zoom/lib/vue-inner-image-zoom.css'
import {useQuasar} from "quasar";
const props = defineProps({ const props = defineProps({
photos: { photos: {
@@ -50,12 +51,17 @@ const prevPhoto = () => {
} }
const showExtra = ref(false); const showExtra = ref(false);
const $q = useQuasar();
const isMaximized = computed(() => $q.screen.width <= $q.screen.sizes.md);
const imageFullscreen = ref(false);
</script> </script>
<template> <template>
<q-dialog <q-dialog
v-model="open" v-model="open"
full-width full-width
:maximized="isMaximized"
> >
<q-card <q-card
v-touch-swipe.mouse.left="prevPhoto" v-touch-swipe.mouse.left="prevPhoto"
@@ -63,10 +69,29 @@ const showExtra = ref(false);
@keyup.left="prevPhoto" @keyup.left="prevPhoto"
@keyup.right="nextPhoto" @keyup.right="nextPhoto"
> >
<q-card-section> <q-card-section class="bg-black text-white" v-if="isMaximized">
<div class="relative-position"> <div class="row">
<div class="col">
<strong v-if="photo.name !== ''">{{ photo.name }}</strong>
</div>
<div class="col text-right">
<q-btn round text-color="white" size="sm" icon="close" @click="open = false"/>
</div>
</div>
</q-card-section>
<q-card-section :class="{'q-pa-none': isMaximized}">
<div :class="{'relative-position': true, 'image-full-height': imageFullscreen}">
<div class="text-center"> <div class="text-center">
<inner-image-zoom move-type="pan" :zoomScale="0.75" :hide-hint="true" :src="photo.url"/> <inner-image-zoom
move-type="pan"
:zoomScale="0.75"
:hide-hint="true"
:after-zoom-in="() => imageFullscreen = true"
:after-zoom-out="() => imageFullscreen = false"
:src="photo.url"
/>
</div> </div>
<q-btn <q-btn
@@ -75,14 +100,14 @@ const showExtra = ref(false);
flat flat
icon="info" icon="info"
:text-color="showExtra ? 'primary' : 'white'" :text-color="showExtra ? 'primary' : 'white'"
style="background: rgba(0, 0, 0, 0.25); top: 8px; right: 8px" style="background: rgba(0, 0, 0, 0.25); bottom: 16px; left: 8px"
@click="showExtra = !showExtra" @click="showExtra = !showExtra"
/> />
<q-btn <q-btn
class="absolute all-pointer-events" class="absolute all-pointer-events"
style="background: rgba(0, 0, 0, 0.25); top: 50%; left: 8px;" style="background: rgba(0, 0, 0, 0.25); top: 50%; left: 8px;"
@click="prevPhoto" @click="prevPhoto"
v-if="photoIndex > 0" v-if="photoIndex > 0 && !isMaximized"
icon="chevron_left" icon="chevron_left"
text-color="white" text-color="white"
flat flat
@@ -93,19 +118,18 @@ const showExtra = ref(false);
class="absolute all-pointer-events" class="absolute all-pointer-events"
style="background: rgba(0, 0, 0, 0.25); top: 50%; right: 8px;" style="background: rgba(0, 0, 0, 0.25); top: 50%; right: 8px;"
@click="nextPhoto" @click="nextPhoto"
v-if="photoIndex < photos.length - 1" v-if="photoIndex < photos.length - 1 && !isMaximized"
icon-right="chevron_right" icon-right="chevron_right"
text-color="white" text-color="white"
flat flat
rounded rounded
> >
</q-btn> </q-btn>
<strong v-if="photo.name !== ''">{{ photo.name }}</strong> <strong v-if="photo.name !== '' && !isMaximized">{{ photo.name }}</strong>
<p v-if="photo.description !== ''">{{ photo.description }}</p> <p v-if="photo.description !== ''">{{ photo.description }}</p>
</div> </div>
<div class="row"> <div class="row">
<div class="col-12 q-my-md" v-if="showExtra"> <div class="col-12 q-my-md" v-if="showExtra">
<EmbeddedMap <EmbeddedMap
v-if="photo.gpsLatitude && photo.gpsLongitude" v-if="photo.gpsLatitude && photo.gpsLongitude"
@@ -153,8 +177,20 @@ const showExtra = ref(false);
width: 100%; width: 100%;
} }
.image-full-height .iiz {
height: calc(95vh - 65px) !important;
}
.iiz__img { .iiz__img {
max-height: 90vh !important; max-height: 90vh !important;
margin: 0 auto; margin: 0 auto;
} }
.iiz__img--hidden {
height: 100vh;
}
.iiz__zoom-img--visible {
//height: 125vh !important
}
</style> </style>
@@ -4,6 +4,7 @@ import {setDefaultData} from "src/composables/form/formHandling";
import DialogWrapper from "components/DialogWrapper.vue"; import DialogWrapper from "components/DialogWrapper.vue";
import FormWrapper from "components/FormWrapper.vue"; import FormWrapper from "components/FormWrapper.vue";
import {commonProps} from "src/composables/inputs"; import {commonProps} from "src/composables/inputs";
import ComboboxInput from "components/inputs/ComboboxInput.vue";
const props = defineProps({ const props = defineProps({
photo: { photo: {
@@ -18,7 +19,7 @@ const photoId = computed(() => props.photo?.id || null);
const formData = ref({ const formData = ref({
name: "", name: "",
description: "", description: "",
pointOfInterestId: null, pointOfInterest: null,
isFlightCover: false isFlightCover: false
}); });
@@ -29,6 +30,7 @@ const setup = (() => {
<template> <template>
<DialogWrapper :formData="formData" :object-id="photoId" @show="setup"> <DialogWrapper :formData="formData" :object-id="photoId" @show="setup">
{{ formData }}
<q-img :src="photo.thumbnailUrl"/> <q-img :src="photo.thumbnailUrl"/>
<q-card-section> <q-card-section>
@@ -36,16 +38,10 @@ const setup = (() => {
<q-input v-model="formData.name" label="Název" v-bind="commonProps"/> <q-input v-model="formData.name" label="Název" v-bind="commonProps"/>
<q-input type="textarea" v-model="formData.description" label="Popis" v-bind="commonProps"/> <q-input type="textarea" v-model="formData.description" label="Popis" v-bind="commonProps"/>
{{ locations }} <ComboboxInput
<q-select
:options="locations" :options="locations"
option-label="name"
option-value="id"
emit-value
map-options
v-bind="commonProps" v-bind="commonProps"
v-model="formData.pointOfInterestId" v-model="formData.pointOfInterest"
label="POI" label="POI"
/> />
+62 -10
View File
@@ -1,4 +1,8 @@
<script setup> <script setup>
import {computed, onMounted, ref} from "vue";
import {useQuasar} from "quasar";
import {confirmObject} from "src/composables/dialog";
const props = defineProps({ const props = defineProps({
photos: { photos: {
type: Array type: Array
@@ -10,26 +14,73 @@ const props = defineProps({
} }
}); });
const emits = defineEmits(["edit", "delete", "openDetail"]) const batchEdit = ref({});
onMounted(() => {
for (const photo of props.photos) {
batchEdit.value[photo.id] = false;
}
});
const emits = defineEmits(["edit", "delete", "batch-edit"])
const openEdit = (photo) => { const openEdit = (photo) => {
emits("edit", photo); emits("edit", photo);
}; };
const deletePhoto = (id) => { const $q = useQuasar();
emits("delete", id);
};
const openDetail = (photo, idx) => { const confirmPhotoDelete = (id) => {
emits("openDetail", {photo, idx, allPhotos: props.photos}); $q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
.onOk(() => {
emits("delete", id);
});
} }
const confirmBatchPhotoDelete = () => {
$q.dialog(confirmObject("Opravdu chces smazat vybrané fotky?" + JSON.stringify(batchEdit.value)))
.onOk(() => {
for (const id in batchEdit.value) {
if (!batchEdit.value[id]) continue;
emits("delete", parseInt(id));
batchEdit.value[id] = false;
}
});
}
const handleImageClick = (photo, idx) => {
if (props.showActions) {
batchEdit.value[photo.id] = !batchEdit.value[photo.id];
}
}
const openBatchEdit = () => {
const photos = [];
for (const i in batchEdit.value) {
const photo = props.photos.find(p => p.id === parseInt(i));
if (photo && batchEdit.value[i]) photos.push(photo);
batchEdit.value[i] = false;
}
emits('batch-edit', photos);
}
const isBatchEditAllowed = computed(() => Object.values(batchEdit.value).filter(v => v).length > 0);
</script> </script>
<template> <template>
<div class="row q-my-md">
<div class="col text-right">
<q-btn round flat icon="edit" :color="isBatchEditAllowed ? 'primary' : 'grey'" :disable="!isBatchEditAllowed" @click="openBatchEdit" v-if="showActions"/>
<q-btn round flat icon="delete" :color="isBatchEditAllowed ? 'red' : 'grey'" :disable="!isBatchEditAllowed" @click="confirmBatchPhotoDelete" v-if="showActions"/>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id"> <div class="col-12 col-sm-4 col-md-4 col-lg-3" v-for="(photo, idx) in photos" :key="photo.id">
<q-card class="q-my-sm q-mx-sm"> <q-card :class='{"q-ma-sm": true, "shadow-1": !batchEdit[photo.id], "shadow-10": batchEdit[photo.id]}'>
<q-img :src="photo.thumbnailUrl" @click="openDetail(photo, idx)"> <q-img :src="photo.thumbnailUrl" @click="handleImageClick(photo, idx)">
<div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''"> <div class="absolute-bottom" v-if="photo.name !== '' || photo.description !== ''">
<div class="text-h6" v-if="photo.name !== ''"> <div class="text-h6" v-if="photo.name !== ''">
{{ photo.name }} {{ photo.name }}
@@ -40,8 +91,9 @@ const openDetail = (photo, idx) => {
</div> </div>
</q-img> </q-img>
<q-card-actions v-if="showActions" align="right"> <q-card-actions v-if="showActions" align="right" :class="{'bg-primary': batchEdit[photo.id], 'text-white': batchEdit[photo.id]}">
<div class="col-grow"> <div class="col-grow">
<q-checkbox v-model="batchEdit[photo.id]"/>
<q-icon name="star" v-if="photo.isFlightCover" color="red"/> <q-icon name="star" v-if="photo.isFlightCover" color="red"/>
<q-icon name="place" v-if="photo.pointOfInterestId"/> <q-icon name="place" v-if="photo.pointOfInterestId"/>
<q-icon name="gps_fixed" v-if="photo.gpsLatitude && photo.gpsLongitude"/> <q-icon name="gps_fixed" v-if="photo.gpsLatitude && photo.gpsLongitude"/>
@@ -49,7 +101,7 @@ const openDetail = (photo, idx) => {
</div> </div>
<q-btn flat icon="edit" color="blue" @click="openEdit(photo)"></q-btn> <q-btn flat icon="edit" color="blue" @click="openEdit(photo)"></q-btn>
<q-btn flat icon="delete" color="red" @click="deletePhoto(photo.id)"></q-btn> <q-btn flat icon="delete" color="red" @click="confirmPhotoDelete(photo.id)"></q-btn>
</q-card-actions> </q-card-actions>
</q-card> </q-card>
</div> </div>
+12 -2
View File
@@ -8,11 +8,21 @@ export const confirmObject = (message, title) => ({
export const successObject = (message) => ({ export const successObject = (message) => ({
message, message,
position: "top", position: "top",
color: "positive" color: "positive",
progress: true,
}); });
export const warningObject = (message) => ({
message,
position: "top",
color: "warning",
});
export const errorObject = (message) => ({ export const errorObject = (message) => ({
message, message,
position: "top", position: "top",
color: "negative" color: "negative",
timeout: 5 * 60 * 1000,
actions: [{ icon: 'close', color: 'white', round: true, handler: () => { }}]
}) })
+2
View File
@@ -21,3 +21,5 @@ export const fetchAircrafts = () => {
loading.value = true; loading.value = true;
useAircraftsStore().fetch().finally(() => loading.value = false); useAircraftsStore().fetch().finally(() => loading.value = false);
} }
export const isRequestPending = storeToRefs(useAircraftsStore).isMutationRunning;
+2
View File
@@ -19,3 +19,5 @@ export const fetchCopilots = () => {
loading.value = true; loading.value = true;
return useCopilotsStore().fetch().finally(() => loading.value = false); return useCopilotsStore().fetch().finally(() => loading.value = false);
} }
export const isRequestPending = storeToRefs(useCopilotsStore()).isMutationRunning;
+2
View File
@@ -43,3 +43,5 @@ export const handleDeleteFlight = ($q, flightId, onOkCallback = () => {
useFlightsStore().delete(flightId).then(onOkCallback); useFlightsStore().delete(flightId).then(onOkCallback);
}); });
}; };
export const isRequestPending = storeToRefs(useFlightsStore()).isRequestPending;
+30
View File
@@ -0,0 +1,30 @@
import {ref} from "vue";
import {usePhotosStore} from "stores/Photo";
import {fetchFlight} from "src/composables/objects/flight";
export const openBatchEdit = ref(false);
export const editBatchPhotos = ref([]);
export const openBatchPhotoForm = (photos) => {
openBatchEdit.value = true;
editBatchPhotos.value = photos;
}
export const handleBatchPhotoEdit = (flightId, data) => {
const photoIds = data.photoIds;
delete data.photoIds;
delete data.id;
const promises = [];
for (const id of photoIds) {
const updateData = {};
if (data.name !== '') updateData.name = data.name;
if (data.description !== '') updateData.description = data.description;
if (data.pointOfInterest) updateData.pointOfInterest = data.pointOfInterest;
if (Object.keys(updateData).length === 0) continue;
promises.push(usePhotosStore().update(id, updateData));
}
Promise.all(promises).finally(() => fetchFlight(flightId))
}
+22
View File
@@ -0,0 +1,22 @@
import {usePhotosStore} from "stores/Photo";
import {fetchFlight} from "src/composables/objects/flight";
import {usePointsOfInterestStore} from "stores/PointsOfInterest";
import {ref} from "vue";
export const handlePhotoUpdate = (flightId, formData) => {
const photoId = formData.id;
delete formData.id;
usePhotosStore().update(photoId, formData).then((data) => {
fetchFlight(flightId); // nenacita se mi nove vytvoreny POI z comboboxu
usePointsOfInterestStore().fetch();
});
};
export const openEdit = ref(false);
export const editPhoto = ref(null);
export const openPhotoEdit = (photo) => {
openEdit.value = true;
editPhoto.value = photo;
};
+6 -8
View File
@@ -4,6 +4,8 @@ import {createUploadLink} from 'apollo-upload-client';
import {onError} from "@apollo/client/link/error"; import {onError} from "@apollo/client/link/error";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {Notify} from "quasar";
import {errorObject, warningObject} from "src/composables/dialog";
const uploadLink = createUploadLink({ const uploadLink = createUploadLink({
@@ -14,8 +16,8 @@ const handleRefreshToken = (forward, operation) => {
return fromPromise( return fromPromise(
useUserStore().refreshAccessToken() useUserStore().refreshAccessToken()
.catch((error) => { .catch((error) => {
console.log("neumim refresh"); Notify.create(warningObject("Tvoje přihlášení vypřešlo. Přihlaš se prosím znovu."));
// window.location = "/login"; window.location = "/login";
}) })
) )
.filter((value) => Boolean(value)) .filter((value) => Boolean(value))
@@ -40,17 +42,14 @@ const errorLink = onError(
} }
if (graphQLErrors) { if (graphQLErrors) {
const messages = [];
for (const error of graphQLErrors) { for (const error of graphQLErrors) {
if (error.message === "Not found") { if (error.message === "Not found") {
useAppStore().appStatus = 404; useAppStore().appStatus = 404;
} else if (error.message.includes("401") || error.message.includes("Not authorized")) { } else if (error.message.includes("401") || error.message.includes("Not authorized")) {
return handleRefreshToken(forward, operation); return handleRefreshToken(forward, operation);
} else { } else {
messages.push(error.message); Notify.create(errorObject("API error: " + error.message));
} }
// TODO: zaridit zobrazeni erroru z messages
} }
} }
}); });
@@ -81,8 +80,7 @@ export const apolloWrapper = {
}) })
.then((response) => response) .then((response) => response)
.catch(err => { .catch(err => {
console.log(err.message); console.error("Error during query: ", err)
// console.error("Error during query: ", err)
return err; return err;
}) })
}, },
-1
View File
@@ -61,7 +61,6 @@ fragment flightFullInfo on Flight {
...weatherFullInfo ...weatherFullInfo
} }
aircraft { aircraft {
...aircraftFullInfo ...aircraftFullInfo
} }
+4 -1
View File
@@ -9,7 +9,10 @@ export const FRAGMENT_PHOTO_FULL_INFO = gql`
name name
description description
flightId flightId
pointOfInterestId pointOfInterest {
id
name
}
isFlightCover isFlightCover
gpsLatitude gpsLatitude
gpsLongitude gpsLongitude
+2 -2
View File
@@ -75,11 +75,11 @@
</template> </template>
<script setup> <script setup>
import {onMounted, ref, watch} from 'vue' import {onMounted, ref} from 'vue'
import NavigationButton from "components/navigation/NavigationButton.vue"; import NavigationButton from "components/navigation/NavigationButton.vue";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import BreadcrumbsNavigation from "components/BreadcrumbsNavigation.vue"; import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {useMeta, useQuasar} from "quasar"; import {useMeta, useQuasar} from "quasar";
import AvatarFromPhoto from "components/AvatarFromPhoto.vue"; import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
+1 -1
View File
@@ -36,7 +36,7 @@ import {useUserStore} from "stores/User";
import {onMounted} from "vue"; import {onMounted} from "vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {useMeta} from "quasar"; import {useMeta} from "quasar";
import BreadcrumbsNavigation from "components/BreadcrumbsNavigation.vue"; import BreadcrumbsNavigation from "components/navigation/BreadcrumbsNavigation.vue";
import AvatarFromPhoto from "components/AvatarFromPhoto.vue"; import AvatarFromPhoto from "components/AvatarFromPhoto.vue";
const {pageTitle, breadcrumbs} = storeToRefs(useAppStore()); const {pageTitle, breadcrumbs} = storeToRefs(useAppStore());
@@ -4,29 +4,26 @@ import {useAircraftsStore} from "stores/Aircrafts";
import {useRoute, useRouter} from "vue-router"; import {useRoute, useRouter} from "vue-router";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import AircraftDetailSkeleton from "components/aircrafts/AircraftDetailSkeleton.vue"; import AircraftDetailSkeleton from "components/aircrafts/AircraftDetailSkeleton.vue";
import {confirmObject, successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
import {loading, aircraft, fetchAircraft} from "src/composables/objects/aircraft"; import {loading, aircraft, fetchAircraft} from "src/composables/objects/aircraft";
import FAB from "components/FAB.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import FlightsList from "components/flights/FlightsList.vue"; import FlightsList from "components/flights/FlightsList.vue";
import {copilot} from "src/composables/objects/copilot"; import AircraftDetailCard from "components/aircrafts/AircraftDetailCard.vue";
import {flight} from "src/composables/objects/flight";
const $q = useQuasar();
const $router = useRouter();
const aircraftId = parseInt(useRoute().params.id); const aircraftId = parseInt(useRoute().params.id);
onMounted(() => { onMounted(() => {
fetchAircraft(aircraftId); fetchAircraft(aircraftId);
}) })
const $q = useQuasar(); const deleteAircraft = (id) => {
const confirmDelete = (id) => {
$q.dialog(confirmObject('Opravdu chceš smazat toto letadlo?'))
.onOk(() => {
useAircraftsStore().delete(id).then(() => { useAircraftsStore().delete(id).then(() => {
useRouter().push({name: "aircrafts"}); $router.push({name: "aircrafts"});
$q.notify(successObject("Letadlo uspesne odstraneno")); $q.notify(successObject("Letadlo úspěšně odstraněno"));
}); });
}) }
};
watch(aircraft, (newVal) => { watch(aircraft, (newVal) => {
useAppStore().setPageTitle(newVal.callSign); useAppStore().setPageTitle(newVal.callSign);
@@ -35,47 +32,12 @@ watch(aircraft, (newVal) => {
{label: newVal.callSign} {label: newVal.callSign}
]) ])
}); });
</script> </script>
<template> <template>
<AircraftDetailSkeleton v-if="loading || !aircraft"/> <AircraftDetailSkeleton v-if="loading || !aircraft"/>
<template v-else> <template v-else>
<AircraftDetailCard :aircraft="aircraft" @delete="deleteAircraft" />
<q-card class="q-mx-md q-my-md flight-detail-card">
<q-img :src="aircraft.photoUrl" v-if="aircraft.photoUrl" />
<q-card-actions class="q-my-md">
<div class="col-grow">
<q-btn
flat
:to='{name: "aircraftEdit", params: {id: aircraft.id}}'
icon="edit"
text-color="primary"
label="Upravit"
/>
</div>
<q-btn @click="confirmDelete(aircraft.id)" icon="delete" flat text-color="red" label="Smazat"/>
</q-card-actions>
<q-card-section>
<q-list>
<q-item v-if="aircraft.manufacturer">
Výrobce: {{ aircraft.manufacturer }}
</q-item>
<q-item v-if="aircraft.model">
Model: {{ aircraft.model }}
</q-item>
</q-list>
</q-card-section>
<!-- <q-card-actions align="right">-->
<!-- <q-btn :to='{name: "aircraftEdit", params: {id: aircraft.id}}' icon="edit" text-color="primary" rounded flat/>-->
<!-- <q-btn @click="confirmDelete(aircraft.id)" icon="delete" text-color="red" rounded flat/>-->
<!-- </q-card-actions>-->
</q-card>
<q-card class="flight-detail-card" flat> <q-card class="flight-detail-card" flat>
<h3>Lety</h3> <h3>Lety</h3>
@@ -84,8 +46,6 @@ watch(aircraft, (newVal) => {
</q-banner> </q-banner>
<FlightsList :items="aircraft.flights" v-else/> <FlightsList :items="aircraft.flights" v-else/>
</q-card> </q-card>
<!-- <FAB :router-link='{name: "aircraftEdit", params: {id: aircraft.id}}' mode="edit"/>-->
</template> </template>
+7 -10
View File
@@ -4,6 +4,7 @@ import AircraftsList from "components/aircrafts/AircraftsList.vue";
import {aircrafts, fetchAircrafts, loading} from "src/composables/objects/aircraft"; import {aircrafts, fetchAircrafts, loading} from "src/composables/objects/aircraft";
import FAB from "components/FAB.vue"; import FAB from "components/FAB.vue";
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue"; import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
onMounted(() => { onMounted(() => {
fetchAircrafts(); fetchAircrafts();
@@ -13,17 +14,13 @@ onMounted(() => {
<template> <template>
<BasicListSkeleton v-if="loading"/> <BasicListSkeleton v-if="loading"/>
<div class="q-pa-md q-gutter-sm" v-else-if="!loading && aircrafts.length === 0"> <BasicInfoBanner
<q-banner class="bg-light-blue text-white"> v-if="!loading && aircrafts.length === 0"
<template v-slot:avatar> :route-add="{name: 'addAircraft'}"
<q-icon name="info"/> label-add="Vytvořit letadlo"
</template> >
Aktuálně nemáš přidané žádné letadlo. Aktuálně nemáš přidané žádné letadlo.
<template v-slot:action> </BasicInfoBanner>
<q-btn flat color="white" :to="{name: 'addAircraft'}" label="Vytvořit letadlo"/>
</template>
</q-banner>
</div>
<AircraftsList <AircraftsList
v-else v-else
@@ -3,26 +3,21 @@ import AircraftForm from "components/aircrafts/AircraftForm.vue";
import {useAircraftsStore} from "stores/Aircrafts"; import {useAircraftsStore} from "stores/Aircrafts";
import {useRoute, useRouter} from "vue-router"; import {useRoute, useRouter} from "vue-router";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import {onMounted, ref} from "vue"; import {onMounted} from "vue";
import AircraftEditSkeleton from "components/aircrafts/AircraftEditSkeleton.vue"; import AircraftEditSkeleton from "components/aircrafts/AircraftEditSkeleton.vue";
import {successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
import {aircraft, fetchAircraft, loading} from "src/composables/objects/aircraft"; import {aircraft, fetchAircraft, loading, isRequestPending} from "src/composables/objects/aircraft";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {copilot} from "src/composables/objects/copilot";
const isRequestPending = ref(false);
const $router = useRouter(); const $router = useRouter();
const $q = useQuasar(); const $q = useQuasar();
const aircraftId = parseInt(useRoute().params.id); const aircraftId = parseInt(useRoute().params.id);
const handleSave = (formData) => { const handleSave = (formData) => {
isRequestPending.value = true;
useAircraftsStore().update(aircraftId, formData).then(() => { useAircraftsStore().update(aircraftId, formData).then(() => {
$q.notify(successObject("Letadlo úspěšně upraveno")); $q.notify(successObject("Letadlo úspěšně upraveno"));
$router.push({name: "aircrafts"}); $router.push({name: "aircraftDetail", params: {id: aircraftId}});
}).finally(() => {
isRequestPending.value = false;
}) })
} }
@@ -6,21 +6,17 @@ import {useQuasar} from "quasar";
import {ref} from "vue"; import {ref} from "vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
import {isRequestPending} from "src/composables/objects/aircraft";
const $q = useQuasar(); const $q = useQuasar();
const router = useRouter(); const router = useRouter();
let isRequestPending = ref(false);
const handleSave = (formData) => { const handleSave = (formData) => {
isRequestPending.value = true;
useAircraftsStore().create(formData).then(() => { useAircraftsStore().create(formData).then(() => {
$q.notify(successObject("Letadlo úspěšně vytvořeno")); $q.notify(successObject("Letadlo úspěšně vytvořeno"));
router.push({name: "aircrafts"}); router.push({name: "aircrafts"});
}).finally(() => { });
isRequestPending.value = false; };
})
}
useAppStore().setPageTitle("Nové letadlo"); useAppStore().setPageTitle("Nové letadlo");
useAppStore().setBreadcrumbs([ useAppStore().setBreadcrumbs([
@@ -24,9 +24,11 @@ onMounted(() => {
<template v-else> <template v-else>
<h3>Společné lety</h3> <h3>Společné lety</h3>
<q-banner v-if="copilot.flights.length === 0" class="bg-light-blue text-white"> <q-banner v-if="copilot.flights.length === 0" class="bg-light-blue text-white">
Zatím žádné společné lety Zatím tu nemáš uložené žádné společné lety.
</q-banner> </q-banner>
<FlightsList :items="copilot.flights" v-else/> <FlightsList :items="copilot.flights" v-else/>
<FAB mode="edit" :router-link="{name: 'copilotEdit', params: {id: copilotId}}"/> <FAB mode="edit" :router-link="{name: 'copilotEdit', params: {id: copilotId}}"/>
+5 -2
View File
@@ -5,7 +5,7 @@ import CopilotsList from "components/copilots/CopilotsList.vue";
import FAB from "components/FAB.vue"; import FAB from "components/FAB.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue"; import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
import BasicInfoBanner from "components/BasicInfoBanner.vue"; import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
onMounted(() => { onMounted(() => {
fetchCopilots(); fetchCopilots();
@@ -16,7 +16,10 @@ onMounted(() => {
<template> <template>
<BasicListSkeleton :items-count-min="5" :items-count-max="10" v-if="loading"/> <BasicListSkeleton :items-count-min="5" :items-count-max="10" v-if="loading"/>
<BasicInfoBanner v-else-if="!loading && copilots.length === 0" :router-add="{name: 'addCopilot'}"> <BasicInfoBanner
v-else-if="!loading && copilots.length === 0"
:router-add="{name: 'addCopilot'}"
>
Zatím tu není nikdo. Přidej si let s někým nebo kopilota vytvoř ručně tu. Zatím tu není nikdo. Přidej si let s někým nebo kopilota vytvoř ručně tu.
</BasicInfoBanner> </BasicInfoBanner>
+11 -7
View File
@@ -1,10 +1,10 @@
<script setup> <script setup>
import {onMounted} from "vue"; import {onMounted} from "vue";
import {loading, copilot, fetchCopilot} from "src/composables/objects/copilot"; import {loading, copilot, fetchCopilot, isRequestPending} from "src/composables/objects/copilot";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import CopilotEditSkeleton from "components/copilots/CopilotEditSkeleton.vue"; import CopilotEditSkeleton from "components/copilots/CopilotEditSkeleton.vue";
import CopilotEditForm from "components/copilots/CopilotForm.vue"; import CopilotEditForm from "components/copilots/CopilotForm.vue";
import {useRoute} from "vue-router"; import {useRoute, useRouter} from "vue-router";
import {useCopilotsStore} from "stores/Copilots"; import {useCopilotsStore} from "stores/Copilots";
import {successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
@@ -23,19 +23,23 @@ onMounted(() => {
}); });
const $q = useQuasar(); const $q = useQuasar();
const $router = useRouter();
const handleSave = (formData) => { const handleSave = (formData) => {
useCopilotsStore().update(copilotId, formData).then(() => { useCopilotsStore().update(copilotId, formData).then(() => {
$q.notify(successObject("Kopilot úspěšně upraven")); $q.notify(successObject("Kopilot úspěšně upraven"));
$router.push({name: "copilotDetail", params: {id: copilotId}});
// useRouter().push({name: "copilots"}); });
// TODO: ^^ hazi error
})
} }
</script> </script>
<template> <template>
<CopilotEditSkeleton v-if="loading"/> <CopilotEditSkeleton v-if="loading"/>
<CopilotEditForm v-else :copilot="copilot" @save="handleSave"/> <CopilotEditForm
v-else
:copilot="copilot"
:is-request-pending="isRequestPending"
@save="handleSave"
/>
</template> </template>
+3 -7
View File
@@ -1,24 +1,20 @@
<script setup> <script setup>
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import {ref} from "vue";
import {successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
import {useCopilotsStore} from "stores/Copilots"; import {useCopilotsStore} from "stores/Copilots";
import CopilotForm from "components/copilots/CopilotForm.vue"; import CopilotForm from "components/copilots/CopilotForm.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {isRequestPending} from "src/composables/objects/copilot";
let isRequestPending = ref(false);
const $q = useQuasar(); const $q = useQuasar();
const router = useRouter(); const router = useRouter();
const handleSave = (formData) => { const handleSave = (formData) => {
isRequestPending.value = true;
useCopilotsStore().create(formData).then(() => { useCopilotsStore().create(formData).then(() => {
$q.notify(successObject("Kopilot úspěšně vytvořen")); $q.notify(successObject("Kopilot úspěšně vytvořen"));
router.push({name: "copilots"}); router.push({name: "copilots"});
}).finally(() => { });
isRequestPending.value = false; };
})
}
useAppStore().setPageTitle("Nový spolucestující"); useAppStore().setPageTitle("Nový spolucestující");
useAppStore().setBreadcrumbs([ useAppStore().setBreadcrumbs([
+45 -48
View File
@@ -1,13 +1,13 @@
<script setup> <script setup>
import FlightFormSteps from "components/flights/form/FlightFormSteps.vue"; import FlightFormSteps from "components/flights/form/FlightFormSteps.vue";
import {computed, onMounted, reactive, ref, watch} from "vue"; import {onMounted, ref, watch} from "vue";
import {useRoute} from "vue-router"; import {useRoute} from "vue-router";
import {useFlightsStore} from "stores/Flights"; import {useFlightsStore} from "stores/Flights";
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue"; import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
import {useAircraftsStore} from "stores/Aircrafts"; import {useAircraftsStore} from "stores/Aircrafts";
import {usePointsOfInterestStore} from "stores/PointsOfInterest"; import {usePointsOfInterestStore} from "stores/PointsOfInterest";
import {useAirportsStore} from "stores/Airports"; import {useAirportsStore} from "stores/Airports";
import {useMeta, useQuasar} from "quasar"; import {useQuasar} from "quasar";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import {flightName} from "src/composables/objects/flight"; import {flightName} from "src/composables/objects/flight";
import FlightFormCopilot from "components/flights/form/FlightFormCopilot.vue"; import FlightFormCopilot from "components/flights/form/FlightFormCopilot.vue";
@@ -16,17 +16,19 @@ import FlightFormNotes from "components/flights/form/FlightFormNotes.vue";
import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue"; import FlightFormPhotos from "components/flights/form/FlightFormPhotos.vue";
import {usePhotosStore} from "stores/Photo"; import {usePhotosStore} from "stores/Photo";
import PhotosList from "components/photos/PhotosList.vue"; import PhotosList from "components/photos/PhotosList.vue";
import {confirmObject, successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
import PhotoForm from "components/photos/PhotoForm.vue"; import PhotoFormDialog from "components/photos/PhotoFormDialog.vue";
import FlightFormTrack from "components/flights/form/FlightFormTrack.vue"; import FlightFormTrack from "components/flights/form/FlightFormTrack.vue";
import {fetchFlight, flight, loading} from "src/composables/objects/flight"; import {fetchFlight, flight, loading} from "src/composables/objects/flight";
import FlightEditSkeleton from "components/flights/FlightEditSkeleton.vue"; import FlightEditSkeleton from "components/flights/FlightEditSkeleton.vue";
import BatchPhotoFormDialog from "components/photos/BatchPhotoFormDialog.vue";
useMeta({title: "Editace letu"}); import {useAppStore} from "stores/App";
import {editBatchPhotos, handleBatchPhotoEdit, openBatchEdit, openBatchPhotoForm} from "src/composables/photos/batchEdit";
import {editPhoto, handlePhotoUpdate, openEdit, openPhotoEdit} from "src/composables/photos/simpleEdit";
const $q = useQuasar(); const $q = useQuasar();
const route = useRoute(); const $route = useRoute();
const flightId = parseInt(route.params.id); const flightId = parseInt($route.params.id);
const aircrafts = storeToRefs(useAircraftsStore()).items; const aircrafts = storeToRefs(useAircraftsStore()).items;
const locations = storeToRefs(usePointsOfInterestStore()).items; const locations = storeToRefs(usePointsOfInterestStore()).items;
@@ -34,17 +36,26 @@ const airports = storeToRefs(useAirportsStore()).items;
const copilots = storeToRefs(useCopilotsStore()).items; const copilots = storeToRefs(useCopilotsStore()).items;
const formStep = ref(""); const formStep = ref("");
watch(() => route.params.step, (val) => formStep.value = val); watch(() => $route.params.step, (val) => formStep.value = val);
onMounted(() => { onMounted(() => {
formStep.value = route.params.step || "basic"; formStep.value = $route.params.step || "basic";
useAircraftsStore().fetch(); useAircraftsStore().fetch();
usePointsOfInterestStore().fetch(); usePointsOfInterestStore().fetch();
useAirportsStore().fetch(); useAirportsStore().fetch();
useCopilotsStore().fetch(); useCopilotsStore().fetch();
fetchFlight(flightId); fetchFlight(flightId).then(() => {
const flightNameFormatted = flightName(flight.value);
useAppStore().setPageTitle(`Editace - ${flightNameFormatted}`);
useAppStore().setBreadcrumbs([
{to: {name: "myFlights"}, label: "Moje lety"},
{to: {name: "flightDetail", params: {id: flightId}}, label: flightNameFormatted},
{label: "Editace"}
]);
})
}); });
const handleFlightEdit = (data) => { const handleFlightEdit = (data) => {
@@ -56,9 +67,11 @@ const handleFlightEdit = (data) => {
} }
const isRequestPending = storeToRefs(useFlightsStore()).isMutationRunning; const isRequestPending = storeToRefs(useFlightsStore()).isMutationRunning;
const uploadInProgress = ref(false);
const handlePhotosUpload = (data) => { const handlePhotosUpload = (data) => {
const promises = []; const promises = [];
uploadInProgress.value = true;
for (const photo of data.photos) { for (const photo of data.photos) {
promises.push(usePhotosStore().upload(flightId, {photo})); promises.push(usePhotosStore().upload(flightId, {photo}));
} }
@@ -66,47 +79,23 @@ const handlePhotosUpload = (data) => {
Promise.all(promises).then(() => { Promise.all(promises).then(() => {
$q.notify(successObject("Úspěšně nahráno")); $q.notify(successObject("Úspěšně nahráno"));
fetchFlight(flightId); fetchFlight(flightId);
}); }).finally(() => uploadInProgress.value = false);
} }
const _findPhotoIndex = (photoId) => flight.value.photos.findIndex((object) => object.id === photoId); const _findPhotoIndex = (photoId) => flight.value.photos.findIndex((object) => object.id === photoId);
const confirmPhotoDelete = (id) => { const deletePhoto = (id) => {
$q.dialog(confirmObject("Opravdu chces smazat tuto fotku?"))
.onOk(() => {
usePhotosStore().delete(id).then((data) => { usePhotosStore().delete(id).then((data) => {
flight.value.photos.splice(_findPhotoIndex(data.id), 1); flight.value.photos.splice(_findPhotoIndex(data.id), 1);
}); });
});
} }
const handlePhotoUpdate = (formData) => {
const photoId = formData.id;
delete formData.id;
usePhotosStore().update(photoId, formData).then((data) => {
const idx = _findPhotoIndex(data.id);
flight.value.photos[idx] = data;
})
}
const openEdit = ref(false);
const editPhoto = ref(null);
const openPhotoEdit = (photo) => {
openEdit.value = true;
editPhoto.value = photo;
}
</script> </script>
<template> <template>
<FlightEditSkeleton v-if="loading"/> <FlightEditSkeleton v-if="loading"/>
<template v-else> <template v-else>
<q-breadcrumbs :key="loading">
<q-breadcrumbs-el :to="{name: 'myFlights'}">Moje lety</q-breadcrumbs-el>
<q-breadcrumbs-el :to="{name: 'flightDetail', params: {id: flightId}}">{{ flightName(flight) }}</q-breadcrumbs-el>
<q-breadcrumbs-el>Editace</q-breadcrumbs-el>
</q-breadcrumbs>
<FlightFormSteps :router-edit-mode="true" :flightId="flightId"/> <FlightFormSteps :router-edit-mode="true" :flightId="flightId"/>
<q-tab-panels v-model="formStep"> <q-tab-panels v-model="formStep">
@@ -136,23 +125,31 @@ const openPhotoEdit = (photo) => {
<q-tab-panel name="photos"> <q-tab-panel name="photos">
<FlightFormPhotos <FlightFormPhotos
:flight="flight" :flight="flight"
:is-request-pending="isRequestPending" :is-request-pending="isRequestPending || uploadInProgress"
@save="handlePhotosUpload" @save="handlePhotosUpload"
/> />
<PhotoForm
v-model="openEdit"
:locations="locations"
:photo="editPhoto"
@save="handlePhotoUpdate"
/>
Tu nejsou videt fotky po nahrani
<PhotosList <PhotosList
:photos="flight.photos" :photos="flight.photos"
@edit="openPhotoEdit" @edit="openPhotoEdit"
@delete="confirmPhotoDelete" @batch-edit="openBatchPhotoForm"
@delete="deletePhoto"
/>
<!-- Dialogs for photo edit -->
<PhotoFormDialog
v-model="openEdit"
:locations="locations"
:photo="editPhoto"
@save="handlePhotoUpdate(flightId, $event)"
/>
<BatchPhotoFormDialog
v-model="openBatchEdit"
:locations="locations"
:photos="editBatchPhotos"
@save="handleBatchPhotoEdit(flightId, $event)"
@delete="deletePhoto"
/> />
</q-tab-panel> </q-tab-panel>
@@ -11,11 +11,14 @@ import {useFlightsStore} from "stores/Flights";
import PhotoCarousel from "components/flights/PhotoCarousel.vue"; import PhotoCarousel from "components/flights/PhotoCarousel.vue";
import FlightTrackMap from "components/flights/FlightTrackMap.vue"; import FlightTrackMap from "components/flights/FlightTrackMap.vue";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import {storeToRefs} from "pinia";
const $q = useQuasar(); const $q = useQuasar();
const $router = useRouter(); const $router = useRouter();
const flightId = parseInt(useRoute().params.id); const flightId = parseInt(useRoute().params.id);
const loggedUser = storeToRefs(useUserStore()).user;
onMounted(() => { onMounted(() => {
fetchFlight(flightId); fetchFlight(flightId);
}) })
@@ -59,7 +62,6 @@ const copyLinkToClipboard = () => {
$q.notify(successObject("Odkaz zkopírován")) $q.notify(successObject("Odkaz zkopírován"))
}); });
}; };
</script> </script>
@@ -82,6 +84,7 @@ const copyLinkToClipboard = () => {
<q-btn <q-btn
@click="togglePublicVisibility" @click="togglePublicVisibility"
:icon="flight.isPublic ? 'visibility_off' : 'visibility'" :icon="flight.isPublic ? 'visibility_off' : 'visibility'"
:disable="!loggedUser.publicUsername"
flat flat
:label="flight.isPublic ? 'Skrýt' : 'Zveřejnit'" :label="flight.isPublic ? 'Skrýt' : 'Zveřejnit'"
/> />
+7 -10
View File
@@ -6,6 +6,7 @@ import FlightsListSkeleton from "components/flights/FlightsListSkeleton.vue";
import FAB from "components/FAB.vue"; import FAB from "components/FAB.vue";
import {fetchFlights, flights, handleDeleteFlight, loading} from "src/composables/objects/flight"; import {fetchFlights, flights, handleDeleteFlight, loading} from "src/composables/objects/flight";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
const $q = useQuasar(); const $q = useQuasar();
@@ -18,17 +19,13 @@ onMounted(() => {
<template> <template>
<FlightsListSkeleton v-if="loading"/> <FlightsListSkeleton v-if="loading"/>
<div class="q-pa-md q-gutter-sm" v-else-if="!loading && flights.length === 0"> <BasicInfoBanner
<q-banner class="bg-light-blue text-white"> v-else-if="!loading && flights.length === 0"
<template v-slot:avatar> :route-add="{name: 'addFlight'}"
<q-icon name="info"/> label-add="Přidat let"
</template> >
Nemáš zaznamenaný žádný let. Nemáš zaznamenaný žádný let.
<template v-slot:action> </BasicInfoBanner>
<q-btn flat color="white" :to="{name: 'addFlight'}" label="Přidat let"/>
</template>
</q-banner>
</div>
<FlightsList v-else :items="flights" @delete="handleDeleteFlight($q, $event)"/> <FlightsList v-else :items="flights" @delete="handleDeleteFlight($q, $event)"/>
+18 -6
View File
@@ -1,5 +1,5 @@
<script setup> <script setup>
import {onMounted} from "vue"; import {onMounted, ref} from "vue";
import {useAircraftsStore} from "stores/Aircrafts"; import {useAircraftsStore} from "stores/Aircrafts";
import {useFlightsStore} from "stores/Flights"; import {useFlightsStore} from "stores/Flights";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
@@ -9,13 +9,20 @@ import {useAirportsStore} from "stores/Airports";
import FlightFormSteps from "components/flights/form/FlightFormSteps.vue"; import FlightFormSteps from "components/flights/form/FlightFormSteps.vue";
import FlightFormBasic from "components/flights/form/FlightFormBasic.vue"; import FlightFormBasic from "components/flights/form/FlightFormBasic.vue";
import {useAppStore} from "stores/App"; import {useAppStore} from "stores/App";
import {isRequestPending} from "src/composables/objects/flight";
import FlightEditSkeleton from "components/flights/FlightEditSkeleton.vue";
const loading = ref(true);
onMounted(() => { onMounted(() => {
useAircraftsStore().fetch(); const promises = [
usePointsOfInterestStore().fetch(); useAircraftsStore().fetch(),
useFlightsStore().fetch(); usePointsOfInterestStore().fetch(),
useAirportsStore().fetch(); useFlightsStore().fetch(),
useAirportsStore().fetch(),
];
loading.value = true;
Promise.all(promises).finally(() => loading.value = false);
useAppStore().setPageTitle("Nový let"); useAppStore().setPageTitle("Nový let");
useAppStore().setBreadcrumbs([ useAppStore().setBreadcrumbs([
@@ -38,10 +45,15 @@ const handleSave = (formData) => {
<template> <template>
<FlightFormSteps :disabled="['copilot', 'track', 'photos', 'notes']"/> <FlightFormSteps :disabled="['copilot', 'track', 'photos', 'notes']"/>
<FlightEditSkeleton v-if="loading"/>
<template v-else>
<FlightFormBasic <FlightFormBasic
@save="handleSave" @save="handleSave"
:is-request-pending="isRequestPending"
:aircrafts="aircrafts" :aircrafts="aircrafts"
:airports="airports" :airports="airports"
:locations="locations" :locations="locations"
/> />
</template> </template>
</template>
@@ -11,7 +11,7 @@ import {usePointsOfInterestTypeStore} from "stores/PointOfInterestType";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import POIForm from "components/poi/POIForm.vue"; import POIForm from "components/poi/POIForm.vue";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import AccessDeniedBanner from "components/AccessDeniedBanner.vue"; import AccessDeniedBanner from "components/banners/AccessDeniedBanner.vue";
const poiId = parseInt(useRoute().params.id); const poiId = parseInt(useRoute().params.id);
@@ -43,6 +43,9 @@ const handleSave = (data) => {
<template> <template>
<POIEditSkeleton v-if="loading"/> <POIEditSkeleton v-if="loading"/>
<AccessDeniedBanner v-else-if="poi.createdbyId !== loggedUser.id" :route-back="{name: 'poiDetail', params: {id: poiId}}"/> <AccessDeniedBanner
v-else-if="poi.createdById !== loggedUser.id"
:route-back="{name: 'poiDetail', params: {id: poiId}}"
/>
<POIForm :poi="poi" :types="poiTypes" v-else @save="handleSave"/> <POIForm :poi="poi" :types="poiTypes" v-else @save="handleSave"/>
</template> </template>
@@ -1,7 +1,6 @@
<script setup> <script setup>
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {useQuasar} from "quasar"; import {useQuasar} from "quasar";
import {ref} from "vue";
import POIForm from "components/poi/POIForm.vue"; import POIForm from "components/poi/POIForm.vue";
import {usePointsOfInterestStore} from "stores/PointsOfInterest"; import {usePointsOfInterestStore} from "stores/PointsOfInterest";
import {successObject} from "src/composables/dialog"; import {successObject} from "src/composables/dialog";
@@ -17,7 +16,6 @@ const handleSave = (formData) => {
router.push({name: "poi"}); router.push({name: "poi"});
}) })
} }
const isRequestPending = storeToRefs(usePointsOfInterestStore()).isMutationRunning; const isRequestPending = storeToRefs(usePointsOfInterestStore()).isMutationRunning;
useAppStore().setPageTitle("Nové zajímavé místo"); useAppStore().setPageTitle("Nové zajímavé místo");
@@ -9,7 +9,7 @@ import {useAppStore} from "stores/App";
import EmbeddedMap from "components/map/EmbeddedMap.vue"; import EmbeddedMap from "components/map/EmbeddedMap.vue";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import {storeToRefs} from "pinia"; import {storeToRefs} from "pinia";
import BasicInfoBanner from "components/BasicInfoBanner.vue"; import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
import FlightsList from "components/flights/FlightsList.vue"; import FlightsList from "components/flights/FlightsList.vue";
const loggedUser = storeToRefs(useUserStore()).user; const loggedUser = storeToRefs(useUserStore()).user;
+8 -11
View File
@@ -5,6 +5,8 @@ import {useAppStore} from "stores/App";
import POIList from "components/poi/POIList.vue"; import POIList from "components/poi/POIList.vue";
import FAB from "components/FAB.vue"; import FAB from "components/FAB.vue";
import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue"; import BasicListSkeleton from "components/basicSkeletons/BasicListSkeleton.vue";
import {flights} from "src/composables/objects/flight";
import BasicInfoBanner from "components/banners/BasicInfoBanner.vue";
onMounted(() => { onMounted(() => {
fetchPOIList(); fetchPOIList();
@@ -15,19 +17,14 @@ useAppStore().setPageTitle("Zajímavá místa")
<template> <template>
<BasicListSkeleton :items-count-min="5" :items-count-max="10" v-if="loading"/> <BasicListSkeleton :items-count-min="5" :items-count-max="10" v-if="loading"/>
<BasicInfoBanner
<div class="q-pa-md q-gutter-sm" v-else-if="!loading && pointsOfInterest.length === 0"> v-else-if="!loading && pointsOfInterest.length === 0"
<q-banner class="bg-light-blue text-white"> :route-add="{name: 'addPoi'}"
<template v-slot:avatar> label-add="Přidat"
<q-icon name="info"/> >
</template>
Nemáš tu uložené žádné zajímavé místo. Přidej si let, nastav si tam ručně trasu a jednotlivé body se zobrazí Nemáš tu uložené žádné zajímavé místo. Přidej si let, nastav si tam ručně trasu a jednotlivé body se zobrazí
i v tomto seznamu. i v tomto seznamu.
<template v-slot:action> </BasicInfoBanner>
<q-btn flat color="white" :to="{name: 'addPoi'}" label="Přidat"/>
</template>
</q-banner>
</div>
<POIList <POIList
v-else v-else
+11 -5
View File
@@ -2,6 +2,7 @@
import {ref} from "vue"; import {ref} from "vue";
import {useUserStore} from "stores/User"; import {useUserStore} from "stores/User";
import {useRouter} from "vue-router"; import {useRouter} from "vue-router";
import {isRequired, isValidEmail, minLength} from "src/composables/form/rules";
const email = ref(""); const email = ref("");
const password = ref(""); const password = ref("");
@@ -12,6 +13,11 @@ const logUser = () => {
router.push({name: "myFlights"}); router.push({name: "myFlights"});
}); });
}; };
const rules = {
email: [isRequired, isValidEmail],
password: [isRequired, minLength(4)]
}
</script> </script>
<template> <template>
@@ -25,12 +31,12 @@ const logUser = () => {
</q-card-section> </q-card-section>
<q-card-section> <q-card-section>
<q-form class="q-px-sm q-pt-xl" @submit.prevent="logUser()"> <q-form class="q-px-sm q-pt-xl" @submit.prevent="logUser()">
<q-input square clearable v-model="email" type="email" label="Email"> <q-input square clearable v-model="email" :rules="rules.email" type="email" label="E-mail">
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="email"/> <q-icon name="email"/>
</template> </template>
</q-input> </q-input>
<q-input square clearable v-model="password" type="password" label="Password"> <q-input square clearable v-model="password" :rules="rules.password" type="password" label="Password">
<template v-slot:prepend> <template v-slot:prepend>
<q-icon name="lock"/> <q-icon name="lock"/>
</template> </template>
@@ -41,9 +47,9 @@ const logUser = () => {
<q-card-actions class="q-px-lg"> <q-card-actions class="q-px-lg">
<q-btn unelevated size="lg" class="full-width" label="Přihlásit se" @click.prevent="logUser()"/> <q-btn unelevated size="lg" class="full-width" label="Přihlásit se" @click.prevent="logUser()"/>
</q-card-actions> </q-card-actions>
<q-card-section class="text-center q-pa-sm"> <!-- <q-card-section class="text-center q-pa-sm">-->
<router-link :to="{name: 'forgottenPassword'}" class="text-grey-6">Zapomenuté heslo</router-link> <!-- <router-link :to="{name: 'forgottenPassword'}" class="text-grey-6">Zapomenuté heslo</router-link>-->
</q-card-section> <!-- </q-card-section>-->
</q-card> </q-card>
</q-page> </q-page>
+1
View File
@@ -41,6 +41,7 @@ onMounted(() => {
:items="flights" :items="flights"
:displayVisibilityInfo="loggedUser.id === pilot.id" :displayVisibilityInfo="loggedUser.id === pilot.id"
:route-detail="{name: 'publicFlightDetail', params: {username: username}}" :route-detail="{name: 'publicFlightDetail', params: {username: username}}"
class="q-mb-md"
/> />
</template> </template>
</div> </div>
-1
View File
@@ -23,7 +23,6 @@ const registerUser = () => {
}) })
}).catch(({response}) => { }).catch(({response}) => {
console.log(response.detail);
if (response.detail === 'User already exists') { if (response.detail === 'User already exists') {
$q.notify(errorObject("Uživatel s tímto e-mailem je již registrovaný. Zkus se přihlásit.")); $q.notify(errorObject("Uživatel s tímto e-mailem je již registrovaný. Zkus se přihlásit."));
} else { } else {
+9 -7
View File
@@ -6,7 +6,8 @@ import {updateInItems} from "stores/helpers";
export const useCopilotsStore = defineStore('SecondPilots', { export const useCopilotsStore = defineStore('SecondPilots', {
state: () => ({ state: () => ({
items: [] items: [],
isMutationRunning: false,
}), }),
actions: { actions: {
@@ -21,19 +22,20 @@ export const useCopilotsStore = defineStore('SecondPilots', {
}, },
create(formData) { create(formData) {
delete formData['id']; delete formData['id'];
return apolloWrapper.mutate(CREATE_COPILOT, {input: formData}).then(({data}) => {
this.items.push(data.createCopilot);
return data.createCopilot; this.isMutationRunning = true;
}) return apolloWrapper.mutate(CREATE_COPILOT, {input: formData}).then(({data}) => {
const copilot = data.createCopilot;
this.items.push(copilot);
return copilot;
}).finally(() => this.isMutationRunning = false);
}, },
update(id, formData) { update(id, formData) {
return apolloWrapper.mutate(EDIT_COPILOT, {id: id, input: formData}).then(({data}) => { return apolloWrapper.mutate(EDIT_COPILOT, {id: id, input: formData}).then(({data}) => {
const copilot = data.editCopilot; const copilot = data.editCopilot;
updateInItems(this.items, "id", copilot); updateInItems(this.items, "id", copilot);
return copilot; return copilot;
}); }).finally(() => this.isMutationRunning = false);
}, },
} }
}) })
-13
View File
@@ -1,13 +0,0 @@
import {defineStore} from 'pinia'
export const useFlashMessagesStore = defineStore('FlashMessages', {
state: () => ({
items: []
}),
actions: {
add(title, content, type) {
}
}
})
-3
View File
@@ -49,11 +49,8 @@ export const useFlightsStore = defineStore('Flights', {
return apolloWrapper return apolloWrapper
.query(FLIGHTS_LIST, {username}) .query(FLIGHTS_LIST, {username})
.then((resp) => { .then((resp) => {
console.log(resp);
const data = resp.data; const data = resp.data;
this.items = data.flights; this.items = data.flights;
return data.flights; return data.flights;
}).catch(err => console.log(err)); }).catch(err => console.log(err));
}, },
+8 -3
View File
@@ -3,6 +3,9 @@ import {apolloWrapper} from "src/graphql/apollo";
import {DELETE_PHOTO, EDIT_PHOTO, UPLOAD_PHOTO} from "src/graphql/mutations/photo"; import {DELETE_PHOTO, EDIT_PHOTO, UPLOAD_PHOTO} from "src/graphql/mutations/photo";
export const usePhotosStore = defineStore('photos', { export const usePhotosStore = defineStore('photos', {
state: () => ({
isMutationRunning: false,
}),
actions: { actions: {
upload(flightId, formData) { upload(flightId, formData) {
@@ -20,15 +23,17 @@ export const usePhotosStore = defineStore('photos', {
if (!formData.description) { if (!formData.description) {
formData.description = ""; formData.description = "";
} }
this.isMutationRunning = true;
return apolloWrapper.mutate(UPLOAD_PHOTO, {input: formData}).then(({data}) => { return apolloWrapper.mutate(UPLOAD_PHOTO, {input: formData}).then(({data}) => {
return data.uploadPhoto; return data.uploadPhoto;
}) }).finally(() => this.isMutationRunning = false);
}, },
update(id, formData) { update(id, formData) {
this.isMutationRunning = true;
return apolloWrapper.mutate(EDIT_PHOTO, {id, input: formData}).then(({data}) => { return apolloWrapper.mutate(EDIT_PHOTO, {id, input: formData}).then(({data}) => {
return data.editPhoto; return data.editPhoto;
}) }).finally(() => this.isMutationRunning = false);
}, },
delete(photoId) { delete(photoId) {
return apolloWrapper.mutate(DELETE_PHOTO, {id: photoId}).then(({data}) => { return apolloWrapper.mutate(DELETE_PHOTO, {id: photoId}).then(({data}) => {
-1
View File
@@ -13,7 +13,6 @@ import { createPinia } from 'pinia'
export default store((/* { ssrContext } */) => { export default store((/* { ssrContext } */) => {
const pinia = createPinia() const pinia = createPinia()
console.log("Pinia index");
// You can add Pinia plugins here // You can add Pinia plugins here
// pinia.use(SomePiniaPlugin) // pinia.use(SomePiniaPlugin)