Skip to content

Commit

Permalink
FTUE TypeScript (#619)
Browse files Browse the repository at this point in the history
* ➕ Add types for FTUE view

* ➕ Add types for FTUE components

* ➕ Add types for tbpro icons

* ➕ Add types for tbpro elements

* 🔨 Fix several type errors across the code base

* 🔨 Fix optional props type errors

* 🔨 Fix section order

* ➕ Refactor constants and add durations constant
  • Loading branch information
devmount authored Sep 3, 2024
1 parent 3754c55 commit 6ebbcb7
Show file tree
Hide file tree
Showing 33 changed files with 516 additions and 521 deletions.
22 changes: 11 additions & 11 deletions frontend/src/components/CalendarQalendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import 'qalendar/dist/style.css';
import CalendarEvent from '@/elements/calendar/CalendarEvent.vue';
import {
ColorSchemes,
dateFormatStrings,
defaultSlotDuration,
DateFormatStrings,
DEFAULT_SLOT_DURATION,
} from '@/definitions';
import { getLocale, getPreferredTheme, timeFormat } from '@/utils';
import { useRoute, useRouter } from 'vue-router';
Expand Down Expand Up @@ -76,7 +76,7 @@ const emit = defineEmits(['daySelected', 'eventSelected', 'dateChange']);
const timeSlotDuration = computed(() => {
// Duration on slots are fixed, so grab the first one.
// This is the same data on schedule.slot_duration, but we never actually pull that info down to the frontend.
const duration = fixedDuration.value ?? defaultSlotDuration;
const duration = fixedDuration.value ?? DEFAULT_SLOT_DURATION;
if (duration <= 15) {
return qalendarSlotDurations['15'];
}
Expand All @@ -92,7 +92,7 @@ const timeSlotDuration = computed(() => {
* @type {ComputedRef<number>}
*/
const timeSlotHeight = computed(() => {
const duration = fixedDuration.value ?? defaultSlotDuration;
const duration = fixedDuration.value ?? DEFAULT_SLOT_DURATION;
if (duration >= 15) {
return 40;
}
Expand Down Expand Up @@ -208,11 +208,11 @@ const calendarEvents = computed(() => {
colorScheme: processCalendarColorScheme(event.calendar_title, event.calendar_color),
time: {
start: event.all_day
? start.format(dateFormatStrings.qalendarFullDay)
: start.format(dateFormatStrings.qalendar),
? start.format(DateFormatStrings.QalendarFullDay)
: start.format(DateFormatStrings.Qalendar),
end: event.all_day
? end.subtract(1, 'minute').format(dateFormatStrings.qalendarFullDay)
: end.format(dateFormatStrings.qalendar),
? end.subtract(1, 'minute').format(DateFormatStrings.QalendarFullDay)
: end.format(DateFormatStrings.Qalendar),
},
description: event.description,
customData: {
Expand Down Expand Up @@ -240,7 +240,7 @@ const calendarEvents = computed(() => {
const end = start.add(slot.duration, 'minutes');
return {
id: appointment.id ?? start.format(dateFormatStrings.qalendar),
id: appointment.id ?? start.format(DateFormatStrings.Qalendar),
title: !isBookingRoute.value
? appointment.title
: `${start.format(displayFormat)} - ${end.format(displayFormat)}`,
Expand All @@ -249,8 +249,8 @@ const calendarEvents = computed(() => {
appointment?.calendar_color ?? 'rgb(20, 184, 166)',
),
time: {
start: start.format(dateFormatStrings.qalendar),
end: end.format(dateFormatStrings.qalendar),
start: start.format(DateFormatStrings.Qalendar),
end: end.format(DateFormatStrings.Qalendar),
},
description: appointment.details,
with: slot.attendee ? [slot.attendee].map((attendee) => `${attendee.name} <${attendee.email}>`).join(', ') : '',
Expand Down
206 changes: 103 additions & 103 deletions frontend/src/components/DataTable.vue
Original file line number Diff line number Diff line change
@@ -1,106 +1,3 @@
<template>
<div class="flex flex-col items-center justify-center gap-4">
<div class="flex w-full flex-col items-center justify-between gap-4 md:flex-row md:gap-0">
<div>
<span class="font-bold">{{ totalDataLength }}</span> {{ dataName }}
</div>
<div v-for="filter in filters" :key="filter.name">
<label class="flex items-center gap-4 whitespace-nowrap">
{{ filter.name }} {{ t('label.filter') }}:
<select class="rounded-md" @change="(evt) => onColumnFilter(evt, filter)">
<option :value="option.key" v-for="option in filter.options" :key="option.key">
{{ option.name }}
</option>
</select>
</label>
</div>
<list-pagination
:list-length="totalDataLength"
:page-size="pageSize"
@update="updatePage"
/>
</div>
<div class="data-table overflow-x-auto">
<table>
<thead>
<tr>
<th v-if="allowMultiSelect">
<input :checked="paginatedDataList.every((row) => selectedRows.includes(row))" @change="(evt) => onPageSelect(evt, paginatedDataList)" id="select-page-input" class="mr-2" type="checkbox"/>
<label class="select-none cursor-pointer" for="select-page-input">
Select Page
</label>
</th>
<th v-for="column in columns" :key="column.key">{{ column.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(datum, i) in paginatedDataList" :key="datum[dataKey] as unknown as string">
<td v-if="allowMultiSelect">
<input :checked="selectedRows.includes(datum)" type="checkbox" @change="(evt) => onFieldSelect(evt, datum)" />
</td>
<td v-for="(fieldData, fieldKey) in datum" :key="fieldKey" :class="`column-${fieldKey}`">
<span v-if="fieldData.type === TableDataType.Text">
{{ fieldData.value }}
</span>
<span v-else-if="fieldData.type === TableDataType.Code" class="flex items-center gap-4">
<code>{{ fieldData.value }}</code>
<text-button :uid="fieldKey" class="btn-copy" :copy="String(fieldData.value)" :title="t('label.copy')" />
</span>
<span v-else-if="fieldData.type === TableDataType.Bool">
<span v-if="fieldData.value">Yes</span>
<span v-else>No</span>
</span>
<span v-else-if="fieldData.type === TableDataType.Link">
<a :href="fieldData.link" target="_blank">{{ fieldData.value }}</a>
</span>
<span v-else-if="fieldData.type === TableDataType.Button">
<primary-button
v-if="fieldData.buttonType === TableDataButtonType.Primary"
:disabled="fieldData.disabled"
@click="emit('fieldClick', fieldKey, datum)"
>
{{ fieldData.value }}
</primary-button>
<secondary-button
v-else-if="fieldData.buttonType === TableDataButtonType.Secondary"
:disabled="fieldData.disabled"
@click="emit('fieldClick', fieldKey, datum)"
>
{{ fieldData.value }}
</secondary-button>
<caution-button
v-else-if="fieldData.buttonType === TableDataButtonType.Caution"
:disabled="fieldData.disabled"
@click="emit('fieldClick', fieldKey, datum)"
>
{{ fieldData.value }}
</caution-button>
</span>
</td>
</tr>
<tr v-if="loading">
<td :colspan="columnSpan">
<div class="flex w-full justify-center">
<loading-spinner/>
</div>
</td>
</tr>
<tr v-else-if="paginatedDataList.length === 0">
<td :colspan="columnSpan">{{ t('error.dataSourceIsEmpty', {name: dataName}) }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<th :colspan="columnSpan">
<slot name="footer"></slot>
</th>
</tr>
</tfoot>
</table>
</div>
</div>
</template>

<script setup lang="ts">
/**
* Data Table
Expand Down Expand Up @@ -222,3 +119,106 @@ const onColumnFilter = (evt: Event, filter: TableFilter) => {
};
</script>

<template>
<div class="flex flex-col items-center justify-center gap-4">
<div class="flex w-full flex-col items-center justify-between gap-4 md:flex-row md:gap-0">
<div>
<span class="font-bold">{{ totalDataLength }}</span> {{ dataName }}
</div>
<div v-for="filter in filters" :key="filter.name">
<label class="flex items-center gap-4 whitespace-nowrap">
{{ filter.name }} {{ t('label.filter') }}:
<select class="rounded-md" @change="(evt) => onColumnFilter(evt, filter)">
<option :value="option.key" v-for="option in filter.options" :key="option.key">
{{ option.name }}
</option>
</select>
</label>
</div>
<list-pagination
:list-length="totalDataLength"
:page-size="pageSize"
@update="updatePage"
/>
</div>
<div class="data-table overflow-x-auto">
<table>
<thead>
<tr>
<th v-if="allowMultiSelect">
<input :checked="paginatedDataList.every((row) => selectedRows.includes(row))" @change="(evt) => onPageSelect(evt, paginatedDataList)" id="select-page-input" class="mr-2" type="checkbox"/>
<label class="select-none cursor-pointer" for="select-page-input">
Select Page
</label>
</th>
<th v-for="column in columns" :key="column.key">{{ column.name }}</th>
</tr>
</thead>
<tbody>
<tr v-for="(datum, i) in paginatedDataList" :key="datum[dataKey] as unknown as string">
<td v-if="allowMultiSelect">
<input :checked="selectedRows.includes(datum)" type="checkbox" @change="(evt) => onFieldSelect(evt, datum)" />
</td>
<td v-for="(fieldData, fieldKey) in datum" :key="fieldKey" :class="`column-${fieldKey}`">
<span v-if="fieldData.type === TableDataType.Text">
{{ fieldData.value }}
</span>
<span v-else-if="fieldData.type === TableDataType.Code" class="flex items-center gap-4">
<code>{{ fieldData.value }}</code>
<text-button :uid="fieldKey as string" class="btn-copy" :copy="String(fieldData.value)" :title="t('label.copy')" />
</span>
<span v-else-if="fieldData.type === TableDataType.Bool">
<span v-if="fieldData.value">Yes</span>
<span v-else>No</span>
</span>
<span v-else-if="fieldData.type === TableDataType.Link">
<a :href="fieldData.link" target="_blank">{{ fieldData.value }}</a>
</span>
<span v-else-if="fieldData.type === TableDataType.Button">
<primary-button
v-if="fieldData.buttonType === TableDataButtonType.Primary"
:disabled="fieldData.disabled"
@click="emit('fieldClick', fieldKey, datum)"
>
{{ fieldData.value }}
</primary-button>
<secondary-button
v-else-if="fieldData.buttonType === TableDataButtonType.Secondary"
:disabled="fieldData.disabled"
@click="emit('fieldClick', fieldKey, datum)"
>
{{ fieldData.value }}
</secondary-button>
<caution-button
v-else-if="fieldData.buttonType === TableDataButtonType.Caution"
:disabled="fieldData.disabled"
@click="emit('fieldClick', fieldKey, datum)"
>
{{ fieldData.value }}
</caution-button>
</span>
</td>
</tr>
<tr v-if="loading">
<td :colspan="columnSpan">
<div class="flex w-full justify-center">
<loading-spinner/>
</div>
</td>
</tr>
<tr v-else-if="paginatedDataList.length === 0">
<td :colspan="columnSpan">{{ t('error.dataSourceIsEmpty', {name: dataName}) }}</td>
</tr>
</tbody>
<tfoot>
<tr>
<th :colspan="columnSpan">
<slot name="footer"></slot>
</th>
</tr>
</tfoot>
</table>
</div>
</div>
</template>
87 changes: 45 additions & 42 deletions frontend/src/components/FTUE/ConnectCalendars.vue
Original file line number Diff line number Diff line change
@@ -1,52 +1,20 @@
<template>
<div class="content">
<form class="form" autocomplete="off" autofocus @submit.prevent @keyup.enter="onSubmit">
<sync-card class="sync-card" v-model="calendars" :title="t('label.calendar', 2)">
<template v-slot:icon>
<span class="icon-calendar">
<img src="@/assets/svg/icons/calendar.svg" :alt="t('ftue.calendarIcon')" :title="t('ftue.calendarIcon')"/>
</span>
</template>
</sync-card>
</form>
</div>
<div class="buttons">
<secondary-button
class="btn-back"
:title="t('label.back')"
v-if="hasPreviousStep"
:disabled="isLoading"
@click="previousStep()"
>{{ t('label.back') }}
</secondary-button>
<primary-button
class="btn-continue"
:aria-label="continueTitle"
v-if="hasNextStep"
@click="onSubmit()"
:tooltip="!selected ? t('ftue.oneCalendarRequired') : null"
:disabled="isLoading || !selected"
>
{{ t('label.continue') }}
</primary-button>
</div>
</template>

<script setup>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import {
onMounted, inject, ref, computed, watch,
} from 'vue';
import SecondaryButton from '@/tbpro/elements/SecondaryButton.vue';
import { useFTUEStore } from '@/stores/ftue-store';
import { useCalendarStore } from '@/stores/calendar-store.ts';
import { useCalendarStore } from '@/stores/calendar-store';
import { storeToRefs } from 'pinia';
import { callKey } from '@/keys';
import { CalendarItem } from '@/models';
import PrimaryButton from '@/tbpro/elements/PrimaryButton.vue';
import SecondaryButton from '@/tbpro/elements/SecondaryButton.vue';
import SyncCard from '@/tbpro/elements/SyncCard.vue';
const { t } = useI18n();
const call = inject('call');
const call = inject(callKey);
const isLoading = ref(false);
Expand All @@ -58,11 +26,11 @@ const {
const { previousStep, nextStep } = ftueStore;
const calendarStore = useCalendarStore();
const calendars = ref([]);
const selected = computed(() => calendars.value.filter((item) => item.checked).length);
const continueTitle = computed(() => (selected.value ? t('label.continue') : t('ftue.oneCalendarRequired')));
const calendars = ref<CalendarItem[]>([]);
const selectedCount = computed(() => calendars.value.filter((item) => item.checked).length);
const continueTitle = computed(() => (selectedCount.value ? t('label.continue') : t('ftue.oneCalendarRequired')));
watch(selected, (val) => {
watch(selectedCount, (val) => {
if (val === 0) {
warningMessage.value = t('ftue.oneCalendarRequired');
} else {
Expand Down Expand Up @@ -97,6 +65,41 @@ const onSubmit = async () => {
};
</script>

<template>
<div class="content">
<form class="form" autocomplete="off" autofocus @submit.prevent @keyup.enter="onSubmit">
<sync-card class="sync-card" v-model="calendars" :title="t('label.calendar', 2)">
<template v-slot:icon>
<span class="icon-calendar">
<img src="@/assets/svg/icons/calendar.svg" :alt="t('ftue.calendarIcon')" :title="t('ftue.calendarIcon')"/>
</span>
</template>
</sync-card>
</form>
</div>
<div class="buttons">
<secondary-button
class="btn-back"
:title="t('label.back')"
v-if="hasPreviousStep"
:disabled="isLoading"
@click="previousStep()"
>{{ t('label.back') }}
</secondary-button>
<primary-button
class="btn-continue"
:aria-label="continueTitle"
v-if="hasNextStep"
@click="onSubmit()"
:tooltip="!selectedCount ? t('ftue.oneCalendarRequired') : null"
:disabled="isLoading || !selectedCount"
>
{{ t('label.continue') }}
</primary-button>
</div>
</template>

<style scoped>
@import '@/assets/styles/custom-media.pcss';
Expand Down
Loading

0 comments on commit 6ebbcb7

Please sign in to comment.