mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-16 20:22:15 +01:00
add endless scrolling to time overview page
This commit is contained in:
@@ -51,7 +51,7 @@ class TimeEntryIndexRequest extends FormRequest
|
|||||||
'string',
|
'string',
|
||||||
'in:true,false',
|
'in:true,false',
|
||||||
],
|
],
|
||||||
// Limit the number of returned time entries
|
// Limit the number of returned time entries (default: 150)
|
||||||
'limit' => [
|
'limit' => [
|
||||||
'integer',
|
'integer',
|
||||||
'min:1',
|
'min:1',
|
||||||
@@ -59,7 +59,8 @@ class TimeEntryIndexRequest extends FormRequest
|
|||||||
],
|
],
|
||||||
// Filter makes sure that only time entries of a whole date are returned
|
// Filter makes sure that only time entries of a whole date are returned
|
||||||
'only_full_dates' => [
|
'only_full_dates' => [
|
||||||
'boolean',
|
'string',
|
||||||
|
'in:true,false',
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppLayout from '@/Layouts/AppLayout.vue';
|
import AppLayout from '@/Layouts/AppLayout.vue';
|
||||||
import TimeTracker from '@/Components/TimeTracker.vue';
|
import TimeTracker from '@/Components/TimeTracker.vue';
|
||||||
import { computed, onMounted } from 'vue';
|
import { computed, onMounted, ref, watch } from 'vue';
|
||||||
import MainContainer from '@/Pages/MainContainer.vue';
|
import MainContainer from '@/Pages/MainContainer.vue';
|
||||||
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
import { useTimeEntriesStore } from '@/utils/useTimeEntries';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
@@ -9,9 +9,25 @@ import dayjs from 'dayjs';
|
|||||||
import type { TimeEntry } from '@/utils/api';
|
import type { TimeEntry } from '@/utils/api';
|
||||||
import TimeEntryRowHeading from '@/Components/Common/TimeEntry/TimeEntryRowHeading.vue';
|
import TimeEntryRowHeading from '@/Components/Common/TimeEntry/TimeEntryRowHeading.vue';
|
||||||
import TimeEntryRow from '@/Components/Common/TimeEntry/TimeEntryRow.vue';
|
import TimeEntryRow from '@/Components/Common/TimeEntry/TimeEntryRow.vue';
|
||||||
|
import { useElementVisibility } from '@vueuse/core';
|
||||||
|
|
||||||
const timeEntriesStore = useTimeEntriesStore();
|
const timeEntriesStore = useTimeEntriesStore();
|
||||||
const { timeEntries } = storeToRefs(timeEntriesStore);
|
const { timeEntries, allTimeEntriesLoaded } = storeToRefs(timeEntriesStore);
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const loadMoreContainer = ref<HTMLDivElement | null>(null);
|
||||||
|
const isLoadMoreVisible = useElementVisibility(loadMoreContainer);
|
||||||
|
|
||||||
|
watch(isLoadMoreVisible, async (isVisible) => {
|
||||||
|
if (
|
||||||
|
isVisible &&
|
||||||
|
timeEntries.value.length > 0 &&
|
||||||
|
!allTimeEntriesLoaded.value
|
||||||
|
) {
|
||||||
|
loading.value = true;
|
||||||
|
await timeEntriesStore.fetchMoreTimeEntries();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await timeEntriesStore.fetchTimeEntries();
|
await timeEntriesStore.fetchTimeEntries();
|
||||||
@@ -43,5 +59,34 @@ const groupedTimeEntries = computed(() => {
|
|||||||
v-for="entry in value"
|
v-for="entry in value"
|
||||||
:time-entry="entry"></TimeEntryRow>
|
:time-entry="entry"></TimeEntryRow>
|
||||||
</div>
|
</div>
|
||||||
|
<div ref="loadMoreContainer">
|
||||||
|
<div
|
||||||
|
v-if="loading && !allTimeEntriesLoaded"
|
||||||
|
class="flex justify-center items-center py-5 text-white font-medium">
|
||||||
|
<svg
|
||||||
|
class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<circle
|
||||||
|
class="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"></circle>
|
||||||
|
<path
|
||||||
|
class="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span> Loading more time entries... </span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="allTimeEntriesLoaded"
|
||||||
|
class="flex justify-center items-center py-5 text-white font-medium">
|
||||||
|
All time entries are loaded!
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,10 +3,13 @@ import { getCurrentOrganizationId } from '@/utils/useUser';
|
|||||||
import { api } from '../../../openapi.json.client';
|
import { api } from '../../../openapi.json.client';
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref } from 'vue';
|
||||||
import type { TimeEntry } from '@/utils/api';
|
import type { TimeEntry } from '@/utils/api';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
||||||
const timeEntries = ref<TimeEntry[]>(reactive([]));
|
const timeEntries = ref<TimeEntry[]>(reactive([]));
|
||||||
|
|
||||||
|
const allTimeEntriesLoaded = ref(false);
|
||||||
|
|
||||||
async function fetchTimeEntries() {
|
async function fetchTimeEntries() {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
@@ -14,11 +17,40 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
|||||||
params: {
|
params: {
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
},
|
},
|
||||||
|
queries: {
|
||||||
|
only_full_dates: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
timeEntries.value = timeEntriesResponse.data;
|
timeEntries.value = timeEntriesResponse.data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchMoreTimeEntries() {
|
||||||
|
const organizationId = getCurrentOrganizationId();
|
||||||
|
if (organizationId) {
|
||||||
|
const latestTimeEntry =
|
||||||
|
timeEntries.value[timeEntries.value.length - 1];
|
||||||
|
dayjs(latestTimeEntry.start).utc().format('YYYY-MM-DD');
|
||||||
|
|
||||||
|
const timeEntriesResponse = await api.getTimeEntries({
|
||||||
|
params: {
|
||||||
|
organization: organizationId,
|
||||||
|
},
|
||||||
|
queries: {
|
||||||
|
only_full_dates: true,
|
||||||
|
before: dayjs(latestTimeEntry.start).utc().format(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (timeEntriesResponse.data.length > 0) {
|
||||||
|
timeEntries.value = timeEntries.value.concat(
|
||||||
|
timeEntriesResponse.data
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
allTimeEntriesLoaded.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateTimeEntry(timeEntry: TimeEntry) {
|
async function updateTimeEntry(timeEntry: TimeEntry) {
|
||||||
const organizationId = getCurrentOrganizationId();
|
const organizationId = getCurrentOrganizationId();
|
||||||
if (organizationId) {
|
if (organizationId) {
|
||||||
@@ -65,5 +97,7 @@ export const useTimeEntriesStore = defineStore('timeEntries', () => {
|
|||||||
updateTimeEntry,
|
updateTimeEntry,
|
||||||
createTimeEntry,
|
createTimeEntry,
|
||||||
deleteTimeEntry,
|
deleteTimeEntry,
|
||||||
|
fetchMoreTimeEntries,
|
||||||
|
allTimeEntriesLoaded,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user