Skip to content

Commit

Permalink
Login page and data table TypeScript (#611)
Browse files Browse the repository at this point in the history
* ➕ Add types to login view and data table

* ➕ Add missing types to list pagination

* ➕ Add missing types to list pagination
  • Loading branch information
devmount authored Aug 7, 2024
1 parent 8a747d3 commit d5e4979
Show file tree
Hide file tree
Showing 10 changed files with 268 additions and 256 deletions.
83 changes: 36 additions & 47 deletions frontend/src/components/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,42 +31,42 @@
</tr>
</thead>
<tbody>
<tr v-for="datum in paginatedDataList" :key="datum">
<tr v-for="(datum, i) in paginatedDataList" :key="i">
<td v-if="allowMultiSelect">
<input 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">
<span v-if="fieldData.type === TableDataType.Text">
{{ fieldData.value }}
</span>
<span v-else-if="fieldData.type === tableDataType.code" class="flex items-center gap-4">
<span v-else-if="fieldData.type === TableDataType.Code" class="flex items-center gap-4">
<code>{{ fieldData.value }}</code>
<text-button class="btn-copy" :copy="fieldData.value" :title="t('label.copy')" />
<text-button class="btn-copy" :copy="String(fieldData.value)" :title="t('label.copy')" />
</span>
<span v-else-if="fieldData.type === tableDataType.bool">
<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">
<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">
<span v-else-if="fieldData.type === TableDataType.Button">
<primary-button
v-if="fieldData.buttonType === tableDataButtonType.primary"
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"
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"
v-else-if="fieldData.buttonType === TableDataButtonType.Caution"
:disabled="fieldData.disabled"
@click="emit('fieldClick', fieldKey, datum)"
>
Expand Down Expand Up @@ -98,41 +98,31 @@
</div>
</template>

<script setup>
<script setup lang="ts">
/**
* Data Table
* @typedef {{type: tableDataType, value: string, link?: string, buttonType?: tableDataButtonType }} DataField
* @typedef {{name: string, key: string}} DataColumn
* @typedef {{name: string, key: string}} FilterOption
* @typedef {{name: string, options: Array<FilterOption>, fn: function}} Filter
*
* @param allowMultiSelect {boolean} - Displays checkboxes next to each row, and emits the `fieldSelect` event with a list of currently selected rows
* @param dataName {string} - The name for the object being represented on the table
* @param columns {Array<DataColumn>} - List of columns to be displayed (these don't filter data, filter that yourself!)
* @param dataList {Array<DataField>} - List of data to be displayed
* @param filters {Array<Filter>} - List of filters to be displayed
* @param loading {boolean} - Displays a loading spinner
*/
import ListPagination from '@/elements/ListPagination.vue';
import { useI18n } from 'vue-i18n';
import {
computed, ref, toRefs,
} from 'vue';
import { tableDataButtonType, tableDataType } from '@/definitions';
import { computed, ref, toRefs } from 'vue';
import { TableDataButtonType, TableDataType } from '@/definitions';
import { TableDataRow, TableDataColumn, TableFilter, HTMLInputElementEvent } from '@/models';
import ListPagination from '@/elements/ListPagination.vue';
import PrimaryButton from '@/elements/PrimaryButton.vue';
import SecondaryButton from '@/elements/SecondaryButton.vue';
import CautionButton from '@/elements/CautionButton.vue';
import TextButton from '@/elements/TextButton.vue';
import LoadingSpinner from '@/elements/LoadingSpinner.vue';
const props = defineProps({
allowMultiSelect: Boolean,
dataName: String,
dataList: Array,
columns: Array,
filters: Array,
loading: Boolean,
});
// component properties
interface Props {
allowMultiSelect: boolean, // Displays checkboxes next to each row, and emits the `fieldSelect` event with a list of currently selected rows
dataName: string, // The name for the object being represented on the table
columns: TableDataColumn[], // List of columns to be displayed (these don't filter data, filter that yourself!)
dataList: TableDataRow[], // List of data to be displayed
filters: TableFilter[], // List of filters to be displayed
loading: boolean, // Displays a loading spinner
};
const props = defineProps<Props>();
const {
dataList, columns, dataName, allowMultiSelect, loading,
Expand All @@ -144,14 +134,13 @@ const emit = defineEmits(['fieldSelect', 'fieldClick']);
// pagination
const pageSize = 10;
const currentPage = ref(0);
const updatePage = (index) => {
const updatePage = (index: number) => {
currentPage.value = index;
};
const columnSpan = computed(() => (columns.value.length + (allowMultiSelect.value ? 1 : 0)));
const selectedFields = ref([]);
const mutableDataList = ref(null);
const selectedRows = ref<TableDataRow[]>([]);
const mutableDataList = ref<TableDataRow[]>(null);
/**
* Returns either a filtered data list, or the original all nice and paginated
Expand All @@ -177,22 +166,22 @@ const totalDataLength = computed(() => {
return 0;
});
const onFieldSelect = (evt, fieldData) => {
const isChecked = evt?.target?.checked;
const onFieldSelect = (evt: Event, row: TableDataRow) => {
const isChecked = (evt as HTMLInputElementEvent)?.target?.checked;
if (isChecked) {
selectedFields.value.push(fieldData);
selectedRows.value.push(row);
} else {
const index = selectedFields.value.indexOf(fieldData);
const index = selectedRows.value.indexOf(row);
if (index !== -1) {
selectedFields.value.splice(index, 1);
selectedRows.value.splice(index, 1);
}
}
emit('fieldSelect', selectedFields.value);
emit('fieldSelect', selectedRows.value);
};
const onColumnFilter = (evt, filter) => {
mutableDataList.value = filter.fn(evt.target.value, dataList.value);
const onColumnFilter = (evt: Event, filter: TableFilter) => {
mutableDataList.value = filter.fn((evt as HTMLInputElementEvent).target.value, dataList.value);
if (mutableDataList.value === dataList.value) {
mutableDataList.value = null;
}
Expand Down
34 changes: 15 additions & 19 deletions frontend/src/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,32 +255,28 @@ export const qalendarSlotDurations = {

/**
* Used as the session storage key for the location the user wanted to go to before logging in.
* @type {string}
*/
export const loginRedirectKey = 'loginRedirect';

/**
* Data types for table row items
* @enum
* @readonly
*/
export const tableDataType = {
text: 1,
link: 2,
button: 3,
code: 4,
bool: 5,
};
export enum TableDataType {
Text = 1,
Link = 2,
Button = 3,
Code = 4,
Bool = 5,
}

/**
* @enum
* @readonly
* Button types for table data fields
*/
export const tableDataButtonType = {
primary: 1,
secondary: 2,
caution: 3,
};
export enum TableDataButtonType {
Primary = 1,
Secondary = 2,
Caution = 3,
}

/**
* First Time User Experience Steps
Expand Down Expand Up @@ -347,8 +343,8 @@ export default {
scheduleCreationState,
SettingsSections,
subscriberLevels,
tableDataButtonType,
tableDataType,
TableDataButtonType,
TableDataType,
tooltipPosition,
WaitingListAction,
};
12 changes: 6 additions & 6 deletions frontend/src/elements/ListPagination.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,22 +31,22 @@ const next = () => {
emit('update', currentPage.value);
}
};
const goto = (index) => {
const goto = (index: number) => {
currentPage.value = index;
emit('update', currentPage.value);
};
const isVisibleInnerPage = (p) => (currentPage.value === 0 && p === 3)
const isVisibleInnerPage = (p: number) => (currentPage.value === 0 && p === 3)
|| ((currentPage.value === 0 || currentPage.value === 1) && p === 4)
|| (p > currentPage.value - 1 && p < currentPage.value + 3)
|| ((currentPage.value === pageCount.value - 1 || currentPage.value === pageCount.value - 2) && p === pageCount.value - 3)
|| (currentPage.value === pageCount.value - 1 && p === pageCount.value - 2)
|| p === pageCount.value;
const showPageItem = (p) => pageCount.value < 6 || p === 1 || p === 2 || isVisibleInnerPage(p) || p === pageCount.value - 1;
const showFirstEllipsis = (p) => pageCount.value >= 6 && currentPage.value > 2 && p === 2;
const showPageItemLink = (p) => pageCount.value < 6 || p === 1 || isVisibleInnerPage(p);
const showLastEllipsis = (p) => pageCount.value >= 6 && currentPage.value < pageCount.value - 3 && p === pageCount.value - 1;
const showPageItem = (p: number) => pageCount.value < 6 || p === 1 || p === 2 || isVisibleInnerPage(p) || p === pageCount.value - 1;
const showFirstEllipsis = (p: number) => pageCount.value >= 6 && currentPage.value > 2 && p === 2;
const showPageItemLink = (p: number) => pageCount.value < 6 || p === 1 || isVisibleInnerPage(p);
const showLastEllipsis = (p: number) => pageCount.value >= 6 && currentPage.value < pageCount.value - 3 && p === pageCount.value - 1;
</script>

<template>
Expand Down
44 changes: 42 additions & 2 deletions frontend/src/models.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { Dayjs } from 'dayjs';
import { UseFetchReturn } from '@vueuse/core';
import { InviteStatus, WaitingListAction, EventLocationType, CalendarProviders } from './definitions';
import {
InviteStatus,
WaitingListAction,
EventLocationType,
CalendarProviders,
TableDataButtonType,
TableDataType,
} from './definitions';

export type Attendee = {
id?: number;
Expand Down Expand Up @@ -252,12 +259,16 @@ export type Token = {
access_token: string;
token_type: string;
};
export type AuthUrl = {
url: string;
};

// Types and aliases used for our custom createFetch API calls and return types
export type AuthUrlResponse = UseFetchReturn<AuthUrl|Exception>;
export type AppointmentListResponse = UseFetchReturn<Appointment[]>;
export type AppointmentResponse = UseFetchReturn<Appointment>;
export type AvailabilitySlotResponse = UseFetchReturn<SlotAttendee>;
export type BooleanResponse = UseFetchReturn<boolean>;
export type BooleanResponse = UseFetchReturn<boolean|Exception>;
export type BlobResponse = UseFetchReturn<Blob>;
export type CalendarResponse = UseFetchReturn<Calendar|Exception>;
export type CalendarListResponse = UseFetchReturn<Calendar[]>;
Expand All @@ -277,6 +288,31 @@ export type TokenResponse = UseFetchReturn<Token>;
export type WaitingListResponse = UseFetchReturn<WaitingListEntry[]|Exception>;
export type WaitingListActionResponse = UseFetchReturn<WaitingListStatus>;

// Table types
export type TableDataField = {
type: TableDataType;
value: string|number|boolean;
link?: string;
buttonType?: TableDataButtonType;
disabled?: boolean;
};
export type TableDataRow = {
[key:string]: TableDataField
};
export type TableDataColumn = {
name: string;
key: string;
};
export type TableFilterOption = {
name: string;
key: string;
};
export type TableFilter = {
name: string;
options: TableFilterOption[];
fn: (value: string, list: TableDataRow[]) => TableDataRow[];
};

// Utility types
export type Time<T> = {
start: T;
Expand All @@ -299,3 +335,7 @@ export type HTMLElementEvent = Event & {
target: HTMLElement;
currentTarget: HTMLElement;
};
export type HTMLInputElementEvent = Event & {
target: HTMLInputElement;
currentTarget: HTMLInputElement;
};
2 changes: 1 addition & 1 deletion frontend/src/stores/user-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ export const useUserStore = defineStore('user', () => {
const { data: userData, error } = await fetch('me').put(inputData).json();
if (!error.value) {
// update user in store
await updateProfile(userData.value);
updateProfile(userData.value);
await updateSignedUrl(fetch);

return { error: false };
Expand Down
Loading

0 comments on commit d5e4979

Please sign in to comment.