Skip to content

Commit

Permalink
Merge pull request #1412 from CybercentreCanada/AL-2646
Browse files Browse the repository at this point in the history
Feature/SubmissionProfiles (dev)
  • Loading branch information
cccs-rs authored Feb 10, 2025
2 parents 2067160 + 0dc3842 commit 39b1b7d
Show file tree
Hide file tree
Showing 123 changed files with 14,072 additions and 2,525 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@mui/styles": "^5.11.9",
"@mui/x-date-pickers": "^5.0.18",
"@mui/x-tree-view": "^7.6.2",
"@tanstack/react-form": "^0.34.0",
"@vitejs/plugin-react": "^4.2.1",
"@vitejs/plugin-react-swc": "^3.5.0",
"autosuggest-highlight": "^3.3.4",
Expand Down Expand Up @@ -107,6 +108,7 @@
"@testing-library/user-event": "^14.5.1",
"@types/autosuggest-highlight": "^3.2.3",
"@types/dompurify": "^3.0.5",
"@types/flowjs__flow.js": "^2.13.3",
"@types/lodash": "^4.14.195",
"@types/md5": "^2.3.5",
"@types/node": "^20.3.1",
Expand Down Expand Up @@ -141,7 +143,7 @@
"prettier-plugin-organize-imports": "^3.2.2",
"setimmediate": "^1.0.5",
"ts-node": "^10.9.1",
"typescript": "^5.4.5",
"typescript": "^5.6.3",
"vitest-fetch-mock": "^0.2.2"
},
"jest": {
Expand Down
30 changes: 30 additions & 0 deletions src/components/core/TableOfContent/Anchor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React, { useEffect, useId } from 'react';
import { useTableOfContent } from './TableOfContent';

export type AnchorProps = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
anchor?: string;
label?: React.ReactNode;
subheader?: boolean;
disabled?: boolean;
};

export const Anchor: React.FC<AnchorProps> = React.memo(
({ anchor = null, label = '', subheader = false, children = null, disabled = false, ...props }: AnchorProps) => {
const id = useId();
const { loadAnchors } = useTableOfContent();

useEffect(() => {
if (disabled) return;
loadAnchors({ id: anchor || id, label: label.toString(), subheader });
return () => loadAnchors({});
}, [anchor, disabled, id, label, loadAnchors, subheader]);

return disabled ? (
children
) : (
<div data-anchor={anchor || id} {...props}>
{children}
</div>
);
}
);
137 changes: 137 additions & 0 deletions src/components/core/TableOfContent/TableOfContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { createFormContext } from 'components/core/form/createFormContext';
import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react';

export type TableOfContentStore = {
activeID?: string;
anchors?: { id: string; label: string; subheader: boolean }[];
};

const TABLE_OF_CONTENT_STORE: TableOfContentStore = Object.freeze({
activeID: null,
anchors: []
});

const { FormProvider, useForm } = createFormContext<TableOfContentStore>({
defaultValues: structuredClone(TABLE_OF_CONTENT_STORE)
});

export type TableOfContentContextProps = {
rootRef: React.MutableRefObject<HTMLDivElement>;
headerRef: React.MutableRefObject<HTMLDivElement>;
loadAnchors: (props?: { id?: string; label?: string; subheader?: boolean }) => void;
scrollTo: (event: React.SyntheticEvent, activeAnchor: string) => void;
Anchors: React.FC<{ children: (anchors: TableOfContentStore['anchors']) => React.ReactNode }>;
ActiveAnchor: React.FC<{
activeID: TableOfContentStore['activeID'];
children: (active: boolean) => React.ReactNode;
}>;
};

export const TableOfContentContext = React.createContext<TableOfContentContextProps>(null);

export function useTableOfContent(): TableOfContentContextProps {
return useContext(TableOfContentContext);
}

export type TableOfContentProps = {
behavior?: ScrollOptions['behavior'];
children?: React.ReactNode;
};

export const TableOfContent: React.FC<TableOfContentProps> = React.memo(
({ behavior = 'smooth', children = null }: TableOfContentProps) => {
const form = useForm();

const rootRef = useRef<HTMLDivElement>();
const headerRef = useRef<HTMLDivElement>();

const Anchors = useMemo<TableOfContentContextProps['Anchors']>(
() =>
({ children: render }) =>
<form.Subscribe selector={state => state.values.anchors} children={anchors => render(anchors)} />,
[form]
);

const ActiveAnchor = useMemo<TableOfContentContextProps['ActiveAnchor']>(
() =>
({ activeID, children: render }) =>
<form.Subscribe selector={state => activeID === state.values.activeID} children={active => render(active)} />,
[form]
);

const findActive = useCallback(() => {
const elements = rootRef.current?.querySelectorAll('[data-anchor]');
for (let i = elements.length - 1; i >= 0; i--) {
if (
elements.item(i).getBoundingClientRect().top - 2 <=
rootRef.current.getBoundingClientRect().top + headerRef.current.getBoundingClientRect().height
) {
form.setStore(s => {
s.activeID = elements.item(i).getAttribute('data-anchor');
return s;
});
break;
}
}
}, [form]);

const loadAnchors = useCallback<TableOfContentContextProps['loadAnchors']>(
({ id = null, label = null, subheader = false }) => {
form.setStore(s => {
const elements = rootRef.current?.querySelectorAll('[data-anchor]');
const anchors: TableOfContentStore['anchors'] = [];

(elements || []).forEach(element => {
const anchorID = element.getAttribute('data-anchor');
const index = s.anchors.findIndex(a => a.id === anchorID);
if (id === anchorID) anchors.push({ id, label, subheader });
else if (index >= 0) anchors.push(s.anchors[index]);
});

s.activeID = null;
s.anchors = anchors;
return s;
});
},
[form]
);

const scrollTo = useCallback<TableOfContentContextProps['scrollTo']>(
(event, activeAnchor) => {
event.preventDefault();
event.stopPropagation();

const element: HTMLDivElement = rootRef.current.querySelector("[data-anchor='" + activeAnchor + "']");
rootRef.current.scrollTo({
top: element.offsetTop - rootRef.current.offsetTop - headerRef.current.getBoundingClientRect().height,
behavior: behavior
});
},
[behavior]
);

useEffect(() => {
const rootElement = rootRef.current;
if (!rootElement) return;

rootElement.addEventListener('scroll', findActive, false);
return () => {
rootElement.removeEventListener('scroll', findActive, false);
};
}, [findActive]);

return (
<TableOfContentContext.Provider value={{ rootRef, headerRef, loadAnchors, scrollTo, Anchors, ActiveAnchor }}>
{children}
</TableOfContentContext.Provider>
);
}
);

export const TableOfContentProvider = React.memo((props: TableOfContentProps) => (
<FormProvider>
<TableOfContent {...props} />
</FormProvider>
));

export default TableOfContentProvider;
46 changes: 46 additions & 0 deletions src/components/core/form/createFormContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { FormApi, FormOptions, ReactFormApi, Validator } from '@tanstack/react-form';
import { useForm as useTanStackForm } from '@tanstack/react-form';
import React, { createContext, useCallback, useContext } from 'react';

export function createFormContext<
TFormData,
TFormValidator extends Validator<TFormData, string> = Validator<TFormData, string>
>(options: FormOptions<TFormData, TFormValidator>) {
type FormContextProps = (FormApi<TFormData, TFormValidator> & ReactFormApi<TFormData, TFormValidator>) | null;

const FormContext = createContext<FormContextProps>(null);

type FormProviderProps = {
children: React.ReactNode;
onSubmit?: FormOptions<TFormData, TFormValidator>['onSubmit'];
};

const FormProvider = ({ children, onSubmit = () => null }: FormProviderProps) => {
const form = useTanStackForm({
...options,
onSubmit: props => {
'onSubmit' in options ? options.onSubmit(props) : null;
onSubmit(props);
}
});
return <FormContext.Provider value={form}>{children}</FormContext.Provider>;
};

const useForm = () => {
const form = useContext(FormContext);
if (!form) {
throw new Error('Store not found');
}

const setStore = useCallback(
(updater: (data: TFormData) => TFormData) => {
form.store.setState(s => ({ ...s, values: updater(s.values) }));
},
[form.store]
);

return { ...form, setStore };
};

return { FormProvider, useForm };
}
95 changes: 95 additions & 0 deletions src/components/core/form/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
export type Length<T> = T extends { length: infer L } ? L : never;
type Join<T extends unknown[], D extends string> = T extends string[]
? PopFront<T> extends string
? Length<T> extends 1
? `${PopFront<T>}`
: `${PopFront<T>}${D}${Join<Shift<T>, D>}`
: never
: never;
type Pop<T extends unknown[]> = T extends [...infer R, infer U] ? U : never;
type PopFront<T extends unknown[]> = T extends [infer U, ...infer R] ? U : never;
type Shift<T extends unknown[]> = T extends [infer U, ...infer R] ? R : never;

type Filter<T extends unknown[], U> = T extends []
? []
: T extends [infer F, ...infer R]
? F extends U
? Filter<R, T>
: [F, ...Filter<R, U>]
: never;
type TupleIncludes<T extends unknown[], U> = Length<Filter<T, U>> extends Length<T> ? false : true;
type StringIncludes<S extends string, D extends string> = S extends `${infer T}${D}${infer U}` ? true : false;
type Includes<T extends unknown[] | string, U> = T extends unknown[]
? TupleIncludes<T, U>
: T extends string
? U extends string
? StringIncludes<T, U>
: never
: never;

export type Split<S extends string, D extends string> = string extends S
? string[]
: S extends ''
? []
: S extends `${infer T}${D}${infer U}`
? [T, ...Split<U, D>]
: [S];

export type ValidPaths<T> = keyof T extends never
? never
: {
[K in keyof T]: T[K] extends never
? never
: T[K] extends Record<string | number | symbol, unknown>
? K extends string
? `${K}.${ValidPaths<T[K]>}` | K
: never
: K;
}[keyof T] &
string;

export type ValidPathTuples<T> = keyof T extends never
? never
: {
[K in keyof T]: T[K] extends never
? never
: T[K] extends Record<string | number | symbol, unknown>
? [K, ...ValidPathTuples<T[K]>] | [K]
: [K];
}[keyof T];

// string version
export type NestedType<T, P extends string> = Includes<P, '.'> extends true
? PopFront<Split<P, '.'>> extends keyof T
? NestedType<T[PopFront<Split<P, '.'>>], Join<Shift<Split<P, '.'>>, '.'>>
: never
: P extends keyof T
? T[P]
: never;

// tuple version
export type NestedTypeByTuple<T, P extends string[]> = Length<P> extends 1
? Pop<P> extends keyof T
? T[Pop<P>]
: never
: PopFront<P> extends keyof T
? Shift<P> extends string[]
? NestedTypeByTuple<T[PopFront<P>], Shift<P>>
: never
: never;

// String version internally using tuples
// Bonus: Also errors now
export type NestedTypeUsingTuplesAgain<T, P extends ValidPaths<T>> = NestedTypeByTuple<T, Split<P, '.'>>;

export type NestedTypeUsingTuplesAgain2<T, P extends ValidPathTuples<T>> = NestedType<T, Join<P, '.'>>;

export type NestedKeyOf<T extends object> = ValidPathTuples<T>;

export type NestedKeyOf2<T extends object, P extends unknown[] = []> = {
[K in keyof T]: T[K] extends object ? [...P, K] | NestedKeyOf2<T[K], [...P, K]> : [...P, K];
}[keyof T];

export type ExtractNestedValue<P extends unknown[] = []> = P extends [infer First, ...infer _Rest] ? [First] : never;

export type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T;
43 changes: 43 additions & 0 deletions src/components/core/form/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { expect } from 'vitest';
import type { NestedType } from './models';
import { isObject } from './utils';

// Demo
const dictionary = {
someProp: 123,
nested: {
moreProps: 333,
deeper: {
evenDeeper: {
deepest: 'string'
}
},
alsoDeeper: {
randomProp: {
anotherProp: 'another'
}
}
}
} as const;

type MyDict = typeof dictionary;

type Test = NestedType<MyDict, 'nested.alsoDeeper.randomProp.anotherProp'>; // = yay

const Fn = <T, P extends string>(dict: T, path: P): NestedType<T, P> => {
// skip impl.
return undefined as any;
};

const testFromFn = Fn(dictionary, 'nested.moreProps'); // = 333

describe('Test `Form Utilities`', () => {
it('testing the `isObject()`', () => {
expect(isObject(undefined)).toBe(false);
expect(isObject(null)).toBe(false);
expect(isObject({}, true)).toBe(true);
expect(isObject({}, false)).toBe(false);
expect(isObject([])).toBe(false);
expect(isObject({ test: 'test' })).toBe(true);
});
});
Loading

0 comments on commit 39b1b7d

Please sign in to comment.