Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Login page and data table TypeScript #611

Merged
merged 3 commits into from
Aug 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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