Skip to content

Commit

Permalink
Sortable, Searchable
Browse files Browse the repository at this point in the history
  • Loading branch information
joao-vasconcelos committed Nov 14, 2023
1 parent 159f374 commit 0bf0934
Show file tree
Hide file tree
Showing 12 changed files with 207 additions and 64 deletions.
25 changes: 0 additions & 25 deletions manager/components/JobsExplorer/JobsExplorer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

/* * */

import styles from './JobsExplorer.module.css';
Expand All @@ -11,35 +9,12 @@ import AppVersion from '@/components/AppVersion/AppVersion';
/* * */

export default function JobsExplorer() {
const handleClick = async () => {
try {
await fetch('https://printer.carrismetropolitana.pt/publish', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
owner_name: 'Teste',
owner_email: 'johnyvasconcelos@icloud.com',
owner_lang: 'pt',
gdpr_consent: true,
render_host: 'escolas.carrismetropolitana.pt',
render_path: '803239/render',
filename: 'Teste Nome PDF',
}),
});
} catch (error) {
console.log(error);
}
};

return (
<JobsExplorerContextProvider>
<div className={styles.container}>
<JobsExplorerToolbar />
<JobsExplorerTable />
<AppVersion />
<div onClick={handleClick}>CLICK</div>
</div>
</JobsExplorerContextProvider>
);
Expand Down
32 changes: 26 additions & 6 deletions manager/components/JobsExplorerTable/JobsExplorerTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { useJobsExplorerContext } from 'contexts/JobsExplorerContext';
import JobsExplorerTableHeader from '@/components/JobsExplorerTableHeader/JobsExplorerTableHeader';
import JobsExplorerTableRowItem from '@/components/JobsExplorerTableRowItem/JobsExplorerTableRowItem';
import NoDataLabel from '@/components/NoDataLabel/NoDataLabel';
import useSearch from 'hooks/useSearch';

/* * */

Expand All @@ -28,19 +29,38 @@ export default function JobsExplorerTable() {
//
// C. Transform data

const allJobsFiltered = useMemo(() => {
const allJobsSorted = useMemo(() => {
if (!allJobsData) return [];
if (!jobsExplorerContext.state.status) return allJobsData;
return allJobsData.filter((item) => item.status === jobsExplorerContext.state.status);
}, [allJobsData, jobsExplorerContext.state.status]);
if (!jobsExplorerContext.state.sort_key) return allJobsData;
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
if (jobsExplorerContext.state.sort_direction) return allJobsData.sort((a, b) => collator.compare(a[jobsExplorerContext.state.sort_key], b[jobsExplorerContext.state.sort_key]));
else return allJobsData.sort((a, b) => collator.compare(b[jobsExplorerContext.state.sort_key], a[jobsExplorerContext.state.sort_key]));
}, [allJobsData, jobsExplorerContext.state.sort_key, jobsExplorerContext.state.sort_direction]);

const allJobsFilteredForStatus = useMemo(() => {
if (!allJobsSorted) return [];
if (!jobsExplorerContext.state.status) return allJobsSorted;
return allJobsSorted.filter((item) => item.status === jobsExplorerContext.state.status);
}, [allJobsSorted, jobsExplorerContext.state.status]);

const allJobsFilteredForRenderHost = useMemo(() => {
if (!allJobsFilteredForStatus) return [];
if (!jobsExplorerContext.state.render_host) return allJobsFilteredForStatus;
return allJobsFilteredForStatus.filter((item) => item.render_host === jobsExplorerContext.state.render_host);
}, [allJobsFilteredForStatus, jobsExplorerContext.state.render_host]);

//
// D. Handle search

const allJobsFilteredForSearch = useSearch(jobsExplorerContext.state.search_query, allJobsFilteredForRenderHost, { keys: ['owner_name', 'owner_email', 'render_path', 'filename'] });

//
// D. Render components
// E. Render components

return (
<div className={styles.container}>
<JobsExplorerTableHeader />
<div className={styles.rows}>{allJobsFiltered?.length > 0 ? allJobsFiltered.map((jobData) => <JobsExplorerTableRowItem key={jobData._id} jobData={jobData} />) : <NoDataLabel fill />}</div>
<div className={styles.rows}>{allJobsFilteredForSearch?.length > 0 ? allJobsFilteredForSearch.map((jobData) => <JobsExplorerTableRowItem key={jobData._id} jobData={jobData} />) : <NoDataLabel fill />}</div>
</div>
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,94 @@
/* * */

import styles from './JobsExplorerTableHeader.module.css';
import { useJobsExplorerContext } from 'contexts/JobsExplorerContext';
import JobsExplorerTableRowContainer from '@/components/JobsExplorerTableRowContainer/JobsExplorerTableRowContainer';

/* * */

export default function JobsExplorerTableHeader() {
//

//
// A. Setup variables

const jobsExplorerContext = useJobsExplorerContext();

//
// B. Handle actions

const handleClickColumnHeader = async (columnKey) => {
jobsExplorerContext.setSortKey(columnKey);
};

//
// B. Render components

return (
<div className={styles.container}>
<JobsExplorerTableRowContainer className={styles.container}>
<div className={styles.column} />

<div className={styles.column}>job_id</div>

<div className={styles.column}>status</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('status')}>
status
</div>

<div className={styles.column}>notification_count</div>
<div className={styles.column}>download_count</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('notification_count')}>
notification_count
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('download_count')}>
download_count
</div>

<div className={styles.column}>date_registered</div>
<div className={styles.column}>date_updated</div>
<div className={styles.column}>date_processing</div>
<div className={styles.column}>date_ready</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('date_registered')}>
date_registered
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('date_updated')}>
date_updated
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('date_processing')}>
date_processing
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('date_ready')}>
date_ready
</div>
<div className={styles.column}>date_notified</div>
<div className={styles.column}>date_downloaded</div>
<div className={styles.column}>date_expired</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('date_expired')}>
date_expired
</div>

<div className={styles.column}>owner_name</div>
<div className={styles.column}>owner_email</div>
<div className={styles.column}>owner_lang</div>
<div className={styles.column}>gdpr_consent</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('owner_name')}>
owner_name
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('owner_email')}>
owner_email
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('owner_lang')}>
owner_lang
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('gdpr_consent')}>
gdpr_consent
</div>

<div className={styles.column}>render_host</div>
<div className={styles.column}>render_path</div>
<div className={styles.column}>render_format</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('render_host')}>
render_host
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('render_path')}>
render_path
</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('render_format')}>
render_format
</div>

<div className={styles.column}>filename</div>
<div className={`${styles.column} ${styles.sortable}`} onClick={() => handleClickColumnHeader('filename')}>
filename
</div>
</JobsExplorerTableRowContainer>
</div>
);

//
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/* * */
/* CONTAINER */

.container {
display: flex;
width: 100%;
Expand All @@ -8,7 +11,18 @@
color: var(--reference-1);
}

/* * */
/* COLUMN */

.column {
padding: 10px;
background-color: var(--color-gray-1);
}

.sortable {
cursor: pointer;
}

.sortable:hover {
background-color: var(--color-gray-2);
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export default function JobsExplorerTableRowItem({ jobData }) {
<div className={styles.column}>{jobData.gdpr_consent || '(empty)'}</div>

<div className={styles.column}>{jobData.render_host || '(empty)'}</div>
<div className={styles.column}>{jobData.render_path || '(empty)'}</div>
<div className={styles.column}>{jobData.render_path.substring(0, 30) || '(empty)'}</div>
<div className={styles.column}>{jobData.render_format || '(empty)'}</div>

<div className={styles.column}>{jobData.filename || '(empty)'}</div>
Expand Down
17 changes: 16 additions & 1 deletion manager/components/JobsExplorerToolbar/JobsExplorerToolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import useSWR from 'swr';
import styles from './JobsExplorerToolbar.module.css';
import { useMemo } from 'react';
import { Select } from '@mantine/core';
import { Select, TextInput } from '@mantine/core';
import { useJobsExplorerContext } from 'contexts/JobsExplorerContext';
import JobsExplorerCreate from '@/components/JobsExplorerCreate/JobsExplorerCreate';
import LogoutButton from '@/components/LogoutButton/LogoutButton';
Expand Down Expand Up @@ -33,19 +33,34 @@ export default function JobsExplorerToolbar() {
return [...new Set(allJobsData.map((obj) => obj.status))];
}, [allJobsData]);

const availableRenderHosts = useMemo(() => {
if (!allJobsData) return [];
return [...new Set(allJobsData.map((obj) => obj.render_host))];
}, [allJobsData]);

//
// D. Handle actions

const handleChangeSelectedStatus = (value) => {
jobsExplorerContext.setSelectedStatus(value);
};

const handleChangeSelectedRenderHost = (value) => {
jobsExplorerContext.setSelectedRenderHost(value);
};

const handleChangeSearchQuery = ({ currentTarget }) => {
jobsExplorerContext.setSearchQuery(currentTarget.value);
};

//
// E. Render components

return (
<div className={styles.container}>
<Select data={availableStatuses} placeholder="Status" onChange={handleChangeSelectedStatus} clearable />
<Select data={availableRenderHosts} placeholder="Render Host" onChange={handleChangeSelectedRenderHost} clearable />
<TextInput placeholder="Search..." value={jobsExplorerContext.state.search_query} onChange={handleChangeSearchQuery} />
<JobsExplorerCreate />
<LogoutButton />
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.container {
display: grid;
grid-template-columns: 1fr 1fr auto;
grid-template-columns: 1fr 1fr 2fr 200px auto;
gap: 20px;
align-items: center;
justify-content: center;
Expand Down
26 changes: 25 additions & 1 deletion manager/contexts/JobsExplorerContext.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
'use client';

/* * */

import { createContext, useCallback, useContext, useMemo, useState } from 'react';

/* * */

// A.
// SETUP INITIAL STATE

const initialState = {
sort_key: null,
sort_direction: false,
status: null,
render_host: null,
search_query: '',
};

// B.
Expand Down Expand Up @@ -35,19 +43,35 @@ export function JobsExplorerContextProvider({ children }) {
//
// B. Setup actions

const setSortKey = useCallback((newSortKey) => {
console.log(newSortKey);
setState((prev) => ({ ...prev, sort_key: newSortKey || null, sort_direction: prev.sort_key === newSortKey ? !prev.sort_direction : true }));
}, []);

const setSelectedStatus = useCallback((newStatus) => {
setState((prev) => ({ ...prev, status: newStatus || null }));
}, []);

const setSelectedRenderHost = useCallback((newRenderHost) => {
setState((prev) => ({ ...prev, render_host: newRenderHost || null }));
}, []);

const setSearchQuery = useCallback((newQuery) => {
setState((prev) => ({ ...prev, search_query: newQuery || '' }));
}, []);

//
// C. Setup context object

const contextObject = useMemo(
() => ({
state,
setSortKey,
setSelectedStatus,
setSelectedRenderHost,
setSearchQuery,
}),
[setSelectedStatus, state]
[state, setSortKey, setSelectedStatus, setSelectedRenderHost, setSearchQuery]
);

//
Expand Down
39 changes: 39 additions & 0 deletions manager/hooks/useSearch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { transliterate } from 'inflected';

export default function useSearch(query, data, options) {
//

// Return all data if query is empty
if (!query) return data;

// Normalize the query to remove non-English characters
const normalizedQuery = transliterate(query.toLowerCase());

// Check if 'keys' option is present
if (options && options.keys) {
return data.filter((item) => {
let hasMatch = false;
for (let key of options.keys) {
if (item.hasOwnProperty(key)) {
const value = item[key];
const stringifiedValue = value ? String(value).toLowerCase() : '';
const normalizedValue = transliterate(stringifiedValue);
if (normalizedValue.includes(normalizedQuery)) {
hasMatch = true;
break;
}
}
}
return hasMatch;
});
}

// The case where options is not defined
return data.filter((item) => {
const stringifiedData = Object.values(item).join('').toLowerCase();
const normalizedItemValue = transliterate(stringifiedData);
return normalizedItemValue.includes(normalizedQuery);
});

//
}
Loading

0 comments on commit 0bf0934

Please sign in to comment.