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

Improve issues functionality #1197

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
12 changes: 5 additions & 7 deletions imports/api/makeFilteredCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {type DependencyList} from 'react';
import schema from '../util/schema';

import type Collection from './Collection';
import type Selector from './query/Selector';
import {options} from './query/Options';
import type Options from './query/Options';

Expand All @@ -17,14 +16,16 @@ import type UserFilter from './query/UserFilter';
import observeSetChanges from './query/observeSetChanges';
import type Filter from './query/Filter';
import {publishCursorObserver} from './publication/publishCursors';
import type UserQuery from './query/UserQuery';
import queryToSelectorOptionsPair from './query/queryToSelectorOptionsPair';

const makeFilteredCollection = <
S extends schema.ZodTypeAny,
U = schema.infer<S>,
>(
collection: Collection<schema.infer<S>, U>,
tSchema: S,
filterSelector: Selector<schema.infer<S>> | undefined,
filterSelector: Filter<schema.infer<S>> | undefined,
filterOptions: Options<schema.infer<S>> | undefined,
name: string,
) => {
Expand Down Expand Up @@ -68,11 +69,8 @@ const makeFilteredCollection = <

const Filtered = defineCollection<schema.infer<S>, U>(name);

return (
hookSelector: Selector<schema.infer<S>> = {},
options: Options<schema.infer<S>> | undefined = undefined,
deps: DependencyList = [],
) => {
return (query: UserQuery<schema.infer<S>>, deps: DependencyList) => {
const [hookSelector, options] = queryToSelectorOptionsPair(query);
const isLoading = useSubscription(publication, [null, options ?? null]);
const loadingSubscription = isLoading();
const {loading: loadingResults, results} = useCursor(
Expand Down
2 changes: 1 addition & 1 deletion imports/api/query/Options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import sort from './sort';
type Options<T> = Pick<Mongo.Options<T>, 'fields' | 'sort' | 'skip' | 'limit'>;

/**
* @deprecated Use userFilter instead.
* @deprecated Use userQuery instead.
*/
export const options = <S extends schema.ZodTypeAny>(
tSchema: S,
Expand Down
112 changes: 82 additions & 30 deletions imports/api/query/UserFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,19 @@ export type Condition<T> =
* string types can be searched using a regex in mongo
* array types can be searched using their element type
*/
type AlternativeType<T> = T extends ReadonlyArray<infer U>
? T | Condition<U>
: RegExpOrString<T>;
type AlternativeType<T> = T extends schema.ZodOptional<infer U>
? null | AlternativeType<U>
: T extends ReadonlyArray<infer U>
? ArrayFilter<U, T>
: T extends string
? StringFilter<T>
: T;

type RegExpOrString<T> = T extends string ? BSONRegExp | RegExp | T : T;
type ArrayFilter<U, T extends readonly U[] = readonly U[]> = T | Condition<U>;

type StringFilter<T extends string> = BSONRegExp | RegExp | T;

type NumberFilter<T extends number> = T;

export const condition = <S extends schema.ZodTypeAny>(
tSchema: S,
Expand All @@ -102,27 +110,43 @@ export const condition = <S extends schema.ZodTypeAny>(
const alternativeType = <S extends schema.ZodTypeAny>(
tSchema: S,
): schema.ZodType<AlternativeType<schema.infer<S>>> => {
tSchema = unwrap(tSchema);
const [_tSchema, wrap] = unwrap(tSchema);

if (tSchema instanceof schema.ZodArray) {
return schema.union([tSchema, condition(tSchema.element)]);
if (_tSchema instanceof schema.ZodArray) {
return wrap(arrayFilter(_tSchema));
}

if (_tSchema instanceof schema.ZodString) {
return wrap(stringFilter(_tSchema));
}

if (_tSchema instanceof schema.ZodNumber) {
return wrap(numberFilter(_tSchema));
}

return regExpOrString(tSchema) as any;
return wrap(_tSchema);
};

const regExpOrString = <S extends schema.ZodTypeAny>(
const arrayFilter = <U extends schema.ZodTypeAny, S extends schema.ZodArray<U>>(
tSchema: S,
): schema.ZodType<RegExpOrString<schema.infer<S>>> => {
if (unwrap(tSchema) instanceof schema.ZodString) {
return schema.union([
tSchema,
schema.instanceof(RegExp),
// schema.instanceof(BSONRegExp), // TODO This requires an explicit dependency on bson.
]) as any;
}
): schema.ZodType<ArrayFilter<schema.infer<U>, schema.infer<S>>> => {
return schema.union([tSchema, condition(tSchema.element)]);
};

const stringFilter = <S extends schema.ZodString>(
tSchema: S,
): schema.ZodType<StringFilter<schema.infer<S>>> => {
return schema.union([
tSchema,
schema.instanceof(RegExp),
// schema.instanceof(BSONRegExp), // TODO This requires an explicit dependency on bson.
]);
};

return tSchema;
const numberFilter = <S extends schema.ZodNumber>(
tSchema: S,
): schema.ZodType<NumberFilter<schema.infer<S>>> => {
return schema.union([tSchema, schema.nan()]);
};

export type FilterOperators<TValue> = {
Expand Down Expand Up @@ -187,7 +211,7 @@ export const filterOperators = <S extends schema.ZodTypeAny>(
$nin: schema.array(tSchema.nullable()),
// Logical
$not: schema.lazy(() =>
unwrap(tSchema) instanceof schema.ZodString
unwrap(tSchema)[0] instanceof schema.ZodString
? schema.union(s, schema.instanceof(RegExp))
: s,
),
Expand Down Expand Up @@ -226,27 +250,55 @@ export const filterOperators = <S extends schema.ZodTypeAny>(
return s;
};

const unwrap = <S extends schema.ZodTypeAny>(tSchema: S) => {
if (
tSchema instanceof schema.ZodOptional ||
tSchema instanceof schema.ZodBranded
) {
return unwrap(tSchema.unwrap());
const unwrap = <S extends schema.ZodTypeAny>(tSchema: S) =>
_unwrap(tSchema, (x: S) => x);

const _unwrap = <W extends schema.ZodTypeAny, S extends schema.ZodTypeAny = W>(
tSchema: S,
wrap: (tSchema: S) => W,
) => {
if (tSchema instanceof schema.ZodOptional) {
const [_tSchema, _wrap] = _unwrapOptional(tSchema);
return [_tSchema, (x: typeof _tSchema) => wrap(_wrap(x))] as const;
}

if (tSchema instanceof schema.ZodBranded) {
const [_tSchema, _wrap] = _unwrapBranded(tSchema);
return [_tSchema, (x: typeof _tSchema) => wrap(_wrap(x))] as const;
}

return tSchema;
return [tSchema, wrap] as const;
};

const _unwrapOptional = <
S extends schema.ZodOptional<T>,
T extends schema.ZodTypeAny,
>(
tSchema: S,
) => {
return _unwrap(tSchema.unwrap(), (x) => x.nullable());
};

const _unwrapBranded = <
S extends schema.ZodBranded<T, B>,
T extends schema.ZodTypeAny,
B extends string,
>(
tSchema: S,
) => {
return _unwrap(tSchema.unwrap(), (x) => x.brand<B>());
};

const operator =
<O extends (s: schema.ZodTypeAny) => schema.ZodTypeAny>(op: O) =>
<S extends schema.ZodTypeAny>(tSchema: S) => {
tSchema = unwrap(tSchema);
const [_tSchema, wrap] = unwrap(tSchema);

if (tSchema instanceof schema.ZodUnion) {
return schema.union(tSchema.options.map(op));
if (_tSchema instanceof schema.ZodUnion) {
return wrap(schema.union(_tSchema.options.map(op)));
}

return op(tSchema);
return wrap(op(_tSchema));
};

const $all = operator(<S extends schema.ZodTypeAny>(tSchema: S) => {
Expand Down
2 changes: 1 addition & 1 deletion imports/ui/Routing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export default function Routing() {
<Route element={<Stats />} path="stats" />
<Route element={<SEPAPaymentDetails />} path="sepa" />

<Route element={<Issues />} path="issues" />
<Route element={<Issues />} path="issues/*" />
<Route element={<MergePatientsForm />} path="merge" />

<Route element={<DoctorsListRoutes />} path="doctors/*" />
Expand Down
2 changes: 1 addition & 1 deletion imports/ui/consultations/ConsultationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ const _priceProps = ({
case PriceStatus.SHOULD_NOT_BE_ZERO: {
return {
color: 'warning',
helperText: 'Price is zero but book is not special',
helperText: 'Price is zero but book is not `0`',
focused: true,
};
}
Expand Down
9 changes: 9 additions & 0 deletions imports/ui/consultations/ConsultationsPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import makeConsultationsPage from './makeConsultationsPage';

import useConsultationsAndAppointments from './useConsultationsAndAppointments';

const ConsultationsPage = makeConsultationsPage(
useConsultationsAndAppointments,
);

export default ConsultationsPage;
55 changes: 0 additions & 55 deletions imports/ui/consultations/ConsultationsPage.tsx

This file was deleted.

13 changes: 11 additions & 2 deletions imports/ui/consultations/PagedConsultationsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,24 +16,33 @@ type PagedConsultationsListProps = {
readonly loading?: boolean;
readonly dirty?: boolean;
readonly refresh?: () => void;
readonly LoadingIndicator?: React.ElementType<{}>;
readonly EmptyPage?: React.ElementType<{page: number}>;
} & PropsOf<typeof ConsultationsList>;

const DefaultLoadingIndicator = Loading;
const DefaultEmptyPage = ({page}: {readonly page: number}) => (
<NoContent>{`Nothing to see on page ${page}.`}</NoContent>
);

const PagedConsultationsList = ({
loading = false,
page,
perpage,
items,
refresh = undefined,
dirty = false,
LoadingIndicator = DefaultLoadingIndicator,
EmptyPage = DefaultEmptyPage,
...rest
}: PagedConsultationsListProps) => (
<div>
{loading && items.length === 0 ? (
<Loading />
<LoadingIndicator />
) : items.length > 0 ? (
<ConsultationsList loading={loading} items={items} {...rest} />
) : (
<NoContent>{`Nothing to see on page ${page}.`}</NoContent>
<EmptyPage page={page} />
)}
<Paginator loading={loading} end={items.length < perpage} />
{refresh && dirty && <Refresh onClick={refresh} />}
Expand Down
59 changes: 59 additions & 0 deletions imports/ui/consultations/makeConsultationsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, {type DependencyList} from 'react';

import {type ConsultationDocument} from '../../api/collection/consultations';
import type UserFilter from '../../api/query/UserFilter';

Check warning on line 4 in imports/ui/consultations/makeConsultationsPage.tsx

View check run for this annotation

Codecov / codecov/patch

imports/ui/consultations/makeConsultationsPage.tsx#L4

Added line #L4 was not covered by tests
import type UserQuery from '../../api/query/UserQuery';
import type PropsOf from '../../util/types/PropsOf';

import PagedConsultationsList from './PagedConsultationsList';

type Props = {
readonly url?: string;

Check warning on line 11 in imports/ui/consultations/makeConsultationsPage.tsx

View check run for this annotation

Codecov / codecov/patch

imports/ui/consultations/makeConsultationsPage.tsx#L10-L11

Added lines #L10 - L11 were not covered by tests
readonly page?: number;
readonly perpage?: number;

readonly filter?: UserFilter<ConsultationDocument>;
readonly sort: object;
readonly defaultExpandedFirst?: boolean;
} & Omit<PropsOf<typeof PagedConsultationsList>, 'page' | 'perpage' | 'items'>;

type Hook<T> = (
query: UserQuery<T>,
deps: DependencyList,
) => {loading: boolean; results: T[]};

const makeConsultationsPage =
(useConsultations: Hook<ConsultationDocument>) =>
({
filter = {},
sort,
page = 1,
perpage = 10,
url,
defaultExpandedFirst = false,
...rest
}: Props) => {
const query = {
filter,
sort,
skip: (page - 1) * perpage,
limit: perpage,
};

const deps = [JSON.stringify(query)];

const {loading, results: items} = useConsultations(query, deps);

return (
<PagedConsultationsList
loading={loading}
page={page}
perpage={perpage}
items={items}
defaultExpandedFirst={page === 1 && defaultExpandedFirst}
{...rest}
/>
);
};

export default makeConsultationsPage;
3 changes: 1 addition & 2 deletions imports/ui/consultations/useConsultationEditorState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,7 @@ const _priceStatus = ({
if (priceString === '') return PriceStatus.SHOULD_NOT_BE_EMPTY;
if (!isValidAmount(priceString)) return PriceStatus.SHOULD_NOT_BE_INVALID;
const price = parseAmount(priceString);
if (price === 0 && isRealBookNumber(book))
return PriceStatus.SHOULD_NOT_BE_ZERO;
if (price === 0 && book !== '0') return PriceStatus.SHOULD_NOT_BE_ZERO;
return PriceStatus.OK;
};

Expand Down
Loading
Loading