Skip to content

Commit

Permalink
feat: add collection filters
Browse files Browse the repository at this point in the history
  • Loading branch information
drodil committed Oct 31, 2024
1 parent 457611a commit 0ef70d1
Show file tree
Hide file tree
Showing 12 changed files with 281 additions and 141 deletions.
46 changes: 43 additions & 3 deletions plugins/qeta-backend/src/database/DatabaseQetaStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {
Answers,
AnswersOptions,
AttachmentParameters,
CollectionOptions,
CollectionPostRank,
Collections,
EntitiesResponse,
Expand All @@ -32,6 +31,7 @@ import {
Answer,
Attachment,
Collection,
CollectionsQuery,
Comment,
EntitiesQuery,
filterTags,
Expand Down Expand Up @@ -1612,14 +1612,29 @@ export class DatabaseQetaStore implements QetaStore {

async getCollections(
user_ref: string,
options: CollectionOptions,
options: CollectionsQuery,
filters?: PermissionCriteria<QetaFilters>,
): Promise<Collections> {
const query = this.db('collections');
const query = this.db('collections')
.select('collections.*')
.leftJoin(
'collection_posts',
'collections.id',
'collection_posts.collectionId',
)
.groupBy('collections.id');

if (options.owner) {
query.where('owner', options.owner);
}

if (options.fromDate && options.toDate) {
query.whereBetween('collections.created', [
`${options.fromDate} 00:00:00.000+00`,
`${options.toDate} 23:59:59.999+00`,
]);
}

if (options.searchQuery) {
this.applySearchQuery(
query,
Expand All @@ -1628,11 +1643,36 @@ export class DatabaseQetaStore implements QetaStore {
);
}

if (options.tags) {
const tags = filterTags(options.tags);
tags.forEach((t, i) => {
query.innerJoin(
`post_tags AS qt${i}`,
'collection_posts.postId',
`qt${i}.postId`,
);
query.innerJoin(`tags AS t${i}`, `qt${i}.tagId`, `t${i}.id`);
query.where(`t${i}.tag`, '=', t);
});
}

if (options.entity) {
query.leftJoin(
'post_entities',
'collection_posts.postId',
'post_entities.postId',
);
query.leftJoin('entities', 'post_entities.entityId', 'entities.id');
query.where('entities.entity_ref', '=', options.entity);
}

query.where('owner', user_ref).orWhere('readAccess', 'public');
const totalQuery = query.clone();

if (options.orderBy) {
query.orderBy(options.orderBy, options.order || 'desc');
} else {
query.orderBy('created', 'desc');
}

if (options.limit) {
Expand Down
12 changes: 2 additions & 10 deletions plugins/qeta-backend/src/database/QetaStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
Answer,
Attachment,
Collection,
CollectionsQuery,
Comment,
EntitiesQuery,
GlobalStat,
Expand Down Expand Up @@ -56,15 +57,6 @@ export interface Templates {
total: number;
}

export interface CollectionOptions {
limit?: number;
offset?: number;
owner?: string;
searchQuery?: string;
orderBy?: 'created' | 'owner';
order?: 'desc' | 'asc';
}

export interface PostOptions {
type?: PostType;
limit?: number;
Expand Down Expand Up @@ -521,7 +513,7 @@ export interface QetaStore {

getCollections(
user_ref: string,
options: CollectionOptions,
options: CollectionsQuery,
filters?: PermissionCriteria<QetaFilters>,
): Promise<Collections>;

Expand Down
11 changes: 10 additions & 1 deletion plugins/qeta-backend/src/service/routes/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
AuthorizeResult,
PermissionCriteria,
} from '@backstage/plugin-permission-common';
import { wrapAsync } from './util';
import { validateDateRange, wrapAsync } from './util';

const ajv = new Ajv({ coerceTypes: 'array' });
addFormats(ajv);
Expand All @@ -43,6 +43,15 @@ export const collectionsRoutes = (router: Router, options: RouteOptions) => {
return;
}

const validDate = validateDateRange(
request.query.fromDate as string,
request.query.toDate as string,
);
if (!validDate?.isValid) {
response.status(400).send(validDate);
return;
}

const decision = await authorizeConditional(
request,
qetaReadPostPermission,
Expand Down
6 changes: 5 additions & 1 deletion plugins/qeta-backend/src/service/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,14 @@ export const CollectionsQuerySchema: JSONSchemaType<CollectionsQuery> = {
owner: { type: 'string', nullable: true },
orderBy: {
type: 'string',
enum: ['created', 'owner'],
enum: ['created', 'title'],
nullable: true,
},
order: { type: 'string', enum: ['desc', 'asc'], nullable: true },
tags: { type: 'array', items: { type: 'string' }, nullable: true },
entity: { type: 'string', nullable: true },
fromDate: { type: 'string', nullable: true, format: 'date' },
toDate: { type: 'string', nullable: true, format: 'date' },
},
required: [],
additionalProperties: false,
Expand Down
17 changes: 5 additions & 12 deletions plugins/qeta-common/src/api/QetaApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,14 @@ export interface PaginatedQuery {
limit?: number;
offset?: number;
orderBy?: string;
order?: 'desc' | 'asc';
order?: string;
searchQuery?: string;
}

export interface PostsQuery extends PaginatedQuery {
tags?: string[];
entity?: string;
author?: string;
orderBy?:
| 'rank'
| 'views'
| 'title'
| 'score'
| 'trend'
| 'answersCount'
| 'created'
| 'updated';
order?: 'desc' | 'asc';
noCorrectAnswer?: boolean;
noAnswers?: boolean;
favorite?: boolean;
Expand All @@ -73,7 +63,10 @@ export interface PostsQuery extends PaginatedQuery {

export interface CollectionsQuery extends PaginatedQuery {
owner?: string;
orderBy?: 'created' | 'owner';
entity?: string;
tags?: string[];
fromDate?: string;
toDate?: string;
}

export interface AnswersQuery extends PaginatedQuery {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,18 @@ export const AnswersContainer = (props: AnswersContainerProps) => {
<FilterPanel
onChange={onFilterChange}
filters={filters}
answerFilters
orderByFilters={{
showTrendsOrder: false,
showViewsOrder: false,
showAnswersOrder: false,
showUpdatedOrder: true,
showScoreOrder: true,
}}
quickFilters={{
showNoVotes: true,
showNoCorrectAnswer: false,
showNoAnswers: false,
}}
/>
</Collapse>
)}
Expand Down
129 changes: 89 additions & 40 deletions plugins/qeta-react/src/components/CollectionsGrid/CollectionsGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,38 @@
import React, { useEffect } from 'react';
import { Grid, IconButton, TextField } from '@material-ui/core';
import {
Box,
Button,
Collapse,
Grid,
IconButton,
TextField,
Typography,
} from '@material-ui/core';
import { CollectionsGridContent } from './CollectionsGridContent';
import { useQetaApi, useTranslation } from '../../hooks';
import useDebounce from 'react-use/lib/useDebounce';
import { QetaPagination } from '../QetaPagination/QetaPagination';
import { NoCollectionsCard } from './NoCollectionsCard';
import { FilterKey, FilterPanel, Filters } from '../FilterPanel/FilterPanel';
import FilterList from '@material-ui/icons/FilterList';
import { getFiltersWithDateRange } from '../../utils';

export type CollectionsGridProps = {
owner?: string;
};

type CollectionFilters = {
order: 'asc' | 'desc';
orderBy?: 'created' | 'owner';
searchQuery: string;
owner?: string;
showFilters?: boolean;
};

export const CollectionsGrid = (props: CollectionsGridProps) => {
const { showFilters } = props;
const { t } = useTranslation();
const [page, setPage] = React.useState(1);
const [pageCount, setPageCount] = React.useState(1);
const [searchQuery, setSearchQuery] = React.useState('');
const [collectionsPerPage, setCollectionsPerPage] = React.useState(25);
const [filters, setFilters] = React.useState<CollectionFilters>({
const [showFilterPanel, setShowFilterPanel] = React.useState(false);
const [filters, setFilters] = React.useState<Filters>({
order: 'desc',
searchQuery: '',
owner: props.owner,
orderBy: 'created',
});

const {
Expand All @@ -38,7 +44,7 @@ export const CollectionsGrid = (props: CollectionsGridProps) => {
return api.getCollections({
limit: collectionsPerPage,
offset: (page - 1) * collectionsPerPage,
...filters,
...(getFiltersWithDateRange(filters) as any),
});
},
[collectionsPerPage, page, filters],
Expand All @@ -60,41 +66,84 @@ export const CollectionsGrid = (props: CollectionsGridProps) => {
}
}, [response, collectionsPerPage]);

if (!response?.collections || response.collections.length === 0) {
return <NoCollectionsCard />;
}
const onFilterChange = (key: FilterKey, value: string | string[]) => {
if (filters[key] === value) {
return;
}
setPage(1);
setFilters({ ...filters, ...{ [key]: value } });
};

return (
<Grid container>
<Grid item xs={12}>
<TextField
id="search-bar"
className="text qetaUsersContainerSearchInput"
onChange={(
event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
) => setSearchQuery(event.target.value)}
label={t('collectionsPage.search.label')}
variant="outlined"
placeholder={t('collectionsPage.search.placeholder')}
size="small"
/>
<IconButton type="submit" aria-label="search" />
<Box>
<Grid container justifyContent="space-between">
<Grid item xs={12} md={4}>
<TextField
id="search-bar"
className="text qetaUsersContainerSearchInput"
onChange={(
event: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>,
) => setSearchQuery(event.target.value)}
label={t('collectionsPage.search.label')}
variant="outlined"
placeholder={t('collectionsPage.search.placeholder')}
size="small"
/>
<IconButton type="submit" aria-label="search" />
</Grid>
</Grid>

<Grid container justifyContent="space-between">
{response && (
<Grid item>
<Typography variant="h6" className="qetaCollectionsContainerdCount">
{t('common.collections', { count: response?.total ?? 0 })}
</Typography>
</Grid>
)}
{response && (showFilters ?? true) && (
<Grid item>
<Button
onClick={() => setShowFilterPanel(!showFilterPanel)}
className="qetaCollectionsContainerFilterPanelBtn"
startIcon={<FilterList />}
>
{t('filterPanel.filterButton')}
</Button>
</Grid>
)}
</Grid>
{(showFilters ?? true) && (
<Collapse in={showFilterPanel}>
<FilterPanel
onChange={onFilterChange}
filters={filters}
orderByFilters={{
showTitleOrder: true,
}}
quickFilters={{
showNoAnswers: false,
showNoCorrectAnswer: false,
showNoVotes: false,
}}
/>
</Collapse>
)}
<CollectionsGridContent
loading={loading}
error={error}
response={response}
/>
<QetaPagination
pageSize={collectionsPerPage}
handlePageChange={(_e, p) => setPage(p)}
handlePageSizeChange={e =>
setCollectionsPerPage(Number(e.target.value))
}
page={page}
pageCount={pageCount}
/>
</Grid>
{response && response?.total > 0 && (
<QetaPagination
pageSize={collectionsPerPage}
handlePageChange={(_e, p) => setPage(p)}
handlePageSizeChange={e =>
setCollectionsPerPage(Number(e.target.value))
}
page={page}
pageCount={pageCount}
/>
)}
</Box>
);
};
Loading

0 comments on commit 0ef70d1

Please sign in to comment.