Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • minerva/frontend
1 result
Show changes
Commits on Source (8)
Showing
with 557 additions and 35 deletions
import { openSearchDrawerWithSelectedTab, openSubmapsDrawer } from '@/redux/drawer/drawer.slice';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { reactionsFixture } from '@/models/fixtures/reactionFixture';
import {
openReactionDrawerById,
openSearchDrawerWithSelectedTab,
openSubmapsDrawer,
} from '@/redux/drawer/drawer.slice';
import { getReactionsByIds } from '@/redux/reactions/reactions.thunks';
import { StoreType } from '@/redux/store';
import { getReduxWrapperWithStore } from '@/utils/testing/getReduxWrapperWithStore';
import { act, fireEvent, render, screen } from '@testing-library/react';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import type {} from 'redux-thunk/extend-redux';
import { Drawer } from './Drawer.component';
const renderComponent = (): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore();
const renderComponent = (initialStore?: InitialStoreState): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStore);
return (
render(
<Wrapper>
......@@ -77,4 +88,25 @@ describe('Drawer - component', () => {
expect(screen.getByTestId('submap-drawer')).toBeInTheDocument();
});
});
describe('reaction drawer', () => {
it('should open drawer and display reaction', async () => {
const { id } = reactionsFixture[FIRST_ARRAY_ELEMENT];
const { store } = renderComponent({
reactions: {
data: reactionsFixture,
loading: 'succeeded',
error: { message: '', name: '' },
},
});
expect(screen.queryByTestId('reaction-drawer')).not.toBeInTheDocument();
store.dispatch(getReactionsByIds([id]));
store.dispatch(openReactionDrawerById(id));
await waitFor(() => expect(screen.getByTestId('reaction-drawer')).toBeInTheDocument());
});
});
});
......@@ -2,6 +2,7 @@ import { DRAWER_ROLE } from '@/components/Map/Drawer/Drawer.constants';
import { drawerSelector } from '@/redux/drawer/drawer.selectors';
import { useAppSelector } from '@/redux/hooks/useAppSelector';
import { twMerge } from 'tailwind-merge';
import { ReactionDrawer } from './ReactionDrawer';
import { SearchDrawerWrapper as SearchDrawerContent } from './SearchDrawerWrapper';
import { SubmapsDrawer } from './SubmapsDrawer';
......@@ -11,13 +12,14 @@ export const Drawer = (): JSX.Element => {
return (
<div
className={twMerge(
'absolute bottom-0 left-[88px] top-[104px] z-10 h-calc-drawer w-[432px] -translate-x-full transform bg-white-pearl text-font-500 transition-all duration-500',
'absolute bottom-0 left-[88px] top-[104px] z-10 h-calc-drawer w-[432px] -translate-x-full transform border border-divide bg-white-pearl text-font-500 transition-all duration-500',
isOpen && 'translate-x-0',
)}
role={DRAWER_ROLE}
>
{isOpen && drawerName === 'search' && <SearchDrawerContent />}
{isOpen && drawerName === 'submaps' && <SubmapsDrawer />}
{isOpen && drawerName === 'reaction' && <ReactionDrawer />}
</div>
);
};
import { SECOND_ARRAY_ELEMENT } from '@/constants/common';
import { reactionsFixture } from '@/models/fixtures/reactionFixture';
import { DRAWER_INITIAL_STATE } from '@/redux/drawer/drawer.constants';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { ReactionDrawer } from './ReactionDrawer.component';
import { DEFAULT_REFERENCE_SOURCE } from './ReactionDrawer.constants';
const renderComponent = (initialStoreState: InitialStoreState = {}): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<ReactionDrawer />
</Wrapper>,
),
{
store,
}
);
};
describe('ReactionDrawer - component', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
describe("when there's NO matching reaction", () => {
beforeEach(() =>
renderComponent({
reactions: {
data: [],
loading: 'succeeded',
error: { message: '', name: '' },
},
drawer: DRAWER_INITIAL_STATE,
}),
);
it('should not show drawer content', () => {
expect(screen.queryByText('Reaction:')).toBeNull();
expect(screen.queryByText('Type:')).toBeNull();
expect(screen.queryByText('Annotations:')).toBeNull();
expect(screen.queryByText('Source:')).toBeNull();
});
});
describe('when there IS a matching reaction', () => {
const reaction = reactionsFixture[SECOND_ARRAY_ELEMENT];
const filteredReferences = reaction.references.filter(
ref => ref.link !== null && ref.link !== undefined,
);
const referencesTextHref: [string, string][] = filteredReferences.map(ref => [
`${ref.type} (${ref.id})`,
ref.link as string,
]);
const referencesSources: string[] = filteredReferences.map(
ref => ref.annotatorClassName || DEFAULT_REFERENCE_SOURCE,
);
beforeEach(() =>
renderComponent({
reactions: {
data: reactionsFixture,
loading: 'succeeded',
error: { message: '', name: '' },
},
drawer: {
...DRAWER_INITIAL_STATE,
reactionDrawerState: {
reactionId: reaction.id,
},
},
}),
);
it('should show drawer header', () => {
expect(screen.getByText('Reaction:')).toBeInTheDocument();
expect(screen.getByText(reaction.reactionId)).toBeInTheDocument();
});
it('should show drawer reaction type', () => {
expect(screen.getByText('Type:')).toBeInTheDocument();
expect(screen.getByText(reaction.type)).toBeInTheDocument();
});
it('should show drawer reaction annotations title', () => {
expect(screen.getByText('Annotations:')).toBeInTheDocument();
});
it.each(referencesSources)('should show drawer reaction source for source=%s', source => {
expect(screen.getByText(`Source: ${source}`, { exact: false })).toBeInTheDocument();
});
it.each(referencesTextHref)(
'should show drawer reaction reference with text=%s, href=%s',
(refText, href) => {
const linkReferenceSpan = screen.getByText(refText, { exact: false });
const linkReferenceAnchor = linkReferenceSpan.closest('a');
expect(linkReferenceSpan).toBeInTheDocument();
expect(linkReferenceAnchor).toBeInTheDocument();
expect(linkReferenceAnchor?.href).toBe(`${href}/`); // component render adds trailing slash
},
);
});
});
import {
currentDrawerReactionGroupedReferencesSelector,
currentDrawerReactionSelector,
} from '@/redux/reactions/reactions.selector';
import { DrawerHeading } from '@/shared/DrawerHeading';
import { useSelector } from 'react-redux';
import { ReferenceGroup } from './ReferenceGroup';
export const ReactionDrawer = (): React.ReactNode => {
const reaction = useSelector(currentDrawerReactionSelector);
const referencesGrouped = useSelector(currentDrawerReactionGroupedReferencesSelector);
if (!reaction) {
return null;
}
return (
<div className="h-full max-h-full" data-testid="reaction-drawer">
<DrawerHeading
title={
<>
<span className="font-normal">Reaction:</span>&nbsp;{reaction.reactionId}
</>
}
/>
<div className="flex flex-col gap-6 p-6">
<div className="text-sm font-normal">
Type: <b className="font-semibold">{reaction.type}</b>
</div>
<hr className="border-b border-b-divide" />
<h3 className="font-semibold">Annotations:</h3>
{referencesGrouped.map(group => (
<ReferenceGroup key={group.source} group={group} />
))}
</div>
</div>
);
};
export const DEFAULT_REFERENCE_SOURCE = 'Annotated by curator';
import { StoreType } from '@/redux/store';
import { ReferenceFiltered } from '@/types/reference';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { render, screen } from '@testing-library/react';
import { Props, ReferenceGroup } from './ReferenceGroup.component';
const renderComponent = (
props: Props,
initialStoreState: InitialStoreState = {},
): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
return (
render(
<Wrapper>
<ReferenceGroup {...props} />
</Wrapper>,
),
{
store,
}
);
};
describe('ReactionDrawer - component', () => {
beforeEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
});
const singleReference = {
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
article: {
title:
'The nutrient-responsive transcription factor TFE3 promotes autophagy, lysosomal biogenesis, and clearance of cellular debris.',
authors: [
'Martina JA',
' Diab HI',
' Lishu L',
' Jeong-A L',
' Patange S',
' Raben N',
' Puertollano R.',
],
journal: 'Science signaling',
year: 2014,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
pubmedId: '24448649',
citationCount: 321,
},
type: 'PUBMED',
resource: '24448649',
id: 154973,
annotatorClassName: '',
};
const cases: [string, ReferenceFiltered[]][] = [
['', [singleReference]],
[
'source1',
[
{
...singleReference,
annotatorClassName: 'source1',
id: 1,
},
{
...singleReference,
annotatorClassName: 'source1',
id: 2,
},
],
],
[
'source2',
[
{
...singleReference,
annotatorClassName: 'source2',
id: 3,
},
],
],
];
it.each(cases)('should show reference group with source=%s', (source, references) => {
const referencesTextHref: [string, string][] = references.map(ref => [
`${ref.type} (${ref.id})`,
ref.link as string,
]);
renderComponent({
group: {
source,
references,
},
});
referencesTextHref.forEach(([refText, href]) => {
const linkReferenceSpan = screen.getByText(refText, { exact: false });
const linkReferenceAnchor = linkReferenceSpan.closest('a');
expect(linkReferenceSpan).toBeInTheDocument();
expect(linkReferenceAnchor).toBeInTheDocument();
expect(linkReferenceAnchor?.href).toBe(`${href}`);
});
});
});
import { Icon } from '@/shared/Icon';
import { ReferenceGroup as ReferenceGroupType } from '@/types/reference';
import { DEFAULT_REFERENCE_SOURCE } from '../ReactionDrawer.constants';
export interface Props {
group: ReferenceGroupType;
}
export const ReferenceGroup = ({ group: { source, references } }: Props): JSX.Element => (
<>
<h3 className="font-semibold">Source: {source || DEFAULT_REFERENCE_SOURCE}</h3>
{references.map(({ id, link, type }) => (
<a key={id} href={link} target="_blank">
<div className="flex justify-between">
<span>{`${type} (${id})`}</span>
<Icon name="arrow" className="h-6 w-6 fill-font-500" />
</div>
</a>
))}
</>
);
export { ReferenceGroup } from './ReferenceGroup.component';
export { ReactionDrawer } from './ReactionDrawer.component';
import { render, screen } from '@testing-library/react';
import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture';
import { drugsFixture } from '@/models/fixtures/drugFixtures';
import { mirnasFixture } from '@/models/fixtures/mirnasFixture';
import { StoreType } from '@/redux/store';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { drugsFixture } from '@/models/fixtures/drugFixtures';
import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture';
import { mirnasFixture } from '@/models/fixtures/mirnasFixture';
import { StoreType } from '@/redux/store';
import { PinItem, PinType } from './PinsList.types';
import { render, screen } from '@testing-library/react';
import { PinsList } from './PinsList.component';
import { PinItem, PinTypeWithNone } from './PinsList.types';
const DRUGS_PINS_LIST = drugsFixture.map(drug => ({
id: drug.id,
......@@ -30,7 +30,7 @@ const MIRNA_PINS_LIST = mirnasFixture.map(mirna => ({
const renderComponent = (
pinsList: PinItem[],
type: PinType,
type: PinTypeWithNone,
initialStoreState: InitialStoreState = {},
): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
......
/* eslint-disable no-magic-numbers */
import { render, screen } from '@testing-library/react';
import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture';
import { drugsFixture } from '@/models/fixtures/drugFixtures';
import { StoreType } from '@/redux/store';
import { PinDetailsItem } from '@/types/models';
import {
InitialStoreState,
getReduxWrapperWithStore,
} from '@/utils/testing/getReduxWrapperWithStore';
import { drugsFixture } from '@/models/fixtures/drugFixtures';
import { chemicalsFixture } from '@/models/fixtures/chemicalsFixture';
import { bioEntitiesContentFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { StoreType } from '@/redux/store';
import { PinDetailsItem } from '@/types/models';
import { PinType } from '../PinsList.types';
import { render, screen } from '@testing-library/react';
import { PinTypeWithNone } from '../PinsList.types';
import { PinsListItem } from './PinsListItem.component';
const DRUGS_PIN = {
......@@ -25,7 +25,7 @@ const CHEMICALS_PIN = {
const renderComponent = (
name: string,
pin: PinDetailsItem,
type: PinType,
type: PinTypeWithNone,
initialStoreState: InitialStoreState = {},
): { store: StoreType } => {
const { Wrapper, store } = getReduxWrapperWithStore(initialStoreState);
......
......@@ -43,6 +43,7 @@ describe('SearchDrawerWrapper - component', () => {
listOfBioEnitites: [],
selectedSearchElement: '',
},
reactionDrawerState: {},
},
});
......@@ -61,6 +62,7 @@ describe('SearchDrawerWrapper - component', () => {
listOfBioEnitites: [],
selectedSearchElement: '',
},
reactionDrawerState: {},
},
});
......
import { FIRST, SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { FIRST_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { ELEMENT_SEARCH_RESULT_MOCK_ALIAS } from '@/models/mocks/elementSearchResultMock';
import { apiPath } from '@/redux/apiPath';
......@@ -31,7 +31,7 @@ describe('handleAliasResults - util', () => {
await waitFor(() => {
const actions = store.getActions();
expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
expect(actions[FIRST].type).toEqual('project/getMultiBioEntity/pending');
expect(actions[FIRST_ARRAY_ELEMENT].type).toEqual('project/getMultiBioEntity/pending');
});
});
});
/* eslint-disable no-magic-numbers */
import { SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { FIRST_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { bioEntityResponseFixture } from '@/models/fixtures/bioEntityContentsFixture';
import { reactionsFixture } from '@/models/fixtures/reactionFixture';
import {
......@@ -43,15 +43,22 @@ describe('handleReactionResults - util', () => {
expect(actions[1].type).toEqual('reactions/getByIds/fulfilled');
});
it('should run setBioEntityContent to empty array as second action', () => {
it('should run openReactionDrawerById to empty array as second action', () => {
const actions = store.getActions();
expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
expect(actions[2].type).toEqual('project/getMultiBioEntity/pending');
expect(actions[2].type).toEqual('drawer/openReactionDrawerById');
expect(actions[2].payload).toEqual(reactionsFixture[FIRST_ARRAY_ELEMENT].id);
});
it('should run getBioEntity as third action', () => {
it('should run setBioEntityContent to empty array as third action', () => {
const actions = store.getActions();
expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
expect(actions[3].type).toEqual('project/getBioEntityContents/pending');
expect(actions[3].type).toEqual('project/getMultiBioEntity/pending');
});
it('should run getBioEntity as fourth action', () => {
const actions = store.getActions();
expect(actions.length).toBeGreaterThan(SIZE_OF_EMPTY_ARRAY);
expect(actions[4].type).toEqual('project/getBioEntityContents/pending');
});
});
import { FIRST, SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { FIRST_ARRAY_ELEMENT, SIZE_OF_EMPTY_ARRAY } from '@/constants/common';
import { getMultiBioEntity } from '@/redux/bioEntity/bioEntity.thunks';
import { openReactionDrawerById } from '@/redux/drawer/drawer.slice';
import { getReactionsByIds } from '@/redux/reactions/reactions.thunks';
import { AppDispatch } from '@/redux/store';
import { ElementSearchResult, Reaction } from '@/types/models';
......@@ -15,12 +16,14 @@ export const handleReactionResults =
return;
}
const { products, reactants, modifiers } = payload[FIRST];
const reaction = payload[FIRST_ARRAY_ELEMENT];
const { products, reactants, modifiers } = reaction;
const productsIds = products.map(p => p.aliasId);
const reactantsIds = reactants.map(r => r.aliasId);
const modifiersIds = modifiers.map(m => m.aliasId);
const bioEntitiesIds = [...productsIds, ...reactantsIds, ...modifiersIds].map(identifier => String(identifier));
dispatch(openReactionDrawerById(reaction.id));
await dispatch(
getMultiBioEntity({
searchQueries: bioEntitiesIds,
......
import { FIRST } from '@/constants/common';
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { AppDispatch } from '@/redux/store';
import { ElementSearchResult } from '@/types/models';
import { handleAliasResults } from './handleAliasResults';
......@@ -13,7 +13,7 @@ export const handleSearchResultAction = async ({
searchResults,
dispatch,
}: HandleSearchResultActionInput): Promise<void> => {
const closestSearchResult = searchResults[FIRST];
const closestSearchResult = searchResults[FIRST_ARRAY_ELEMENT];
const { type } = closestSearchResult;
const action = {
ALIAS: handleAliasResults,
......
export const SIZE_OF_EMPTY_ARRAY = 0;
export const ZERO = 0;
export const FIRST = 0;
export const FIRST_ARRAY_ELEMENT = 0;
export const ONE = 1;
export const SECOND_ARRAY_ELEMENT = 1;
import { z } from 'zod';
export const authorSchema = z.string();
export const authorSchema = z.union([
z.string(),
z.object({
firstName: z.string(),
lastName: z.string(),
email: z.string(),
organisation: z.string(),
}),
]);
import { FIRST_ARRAY_ELEMENT } from '@/constants/common';
import { Reference } from '@/types/models';
export const REFERENCES_MOCK_ALL_VALID: Reference[] = [
{
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
article: {
title:
'The nutrient-responsive transcription factor TFE3 promotes autophagy, lysosomal biogenesis, and clearance of cellular debris.',
authors: [
'Martina JA',
' Diab HI',
' Lishu L',
' Jeong-A L',
' Patange S',
' Raben N',
' Puertollano R.',
],
journal: 'Science signaling',
year: 2014,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
pubmedId: '24448649',
citationCount: 321,
},
type: 'PUBMED',
resource: '24448649',
id: 154973,
annotatorClassName: '',
},
{
link: 'https://www.ncbi.nlm.nih.gov/pubmed/27299292',
article: {
title:
'Transcription factor EB: from master coordinator of lysosomal pathways to candidate therapeutic target in degenerative storage diseases.',
authors: ['Sardiello M.'],
journal: 'Annals of the New York Academy of Sciences',
year: 2016,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/27299292',
pubmedId: '27299292',
citationCount: 66,
},
type: 'PUBMED',
resource: '27299292',
id: 154974,
annotatorClassName: '',
},
{
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
article: {
title:
'The nutrient-responsive transcription factor TFE3 promotes autophagy, lysosomal biogenesis, and clearance of cellular debris.',
authors: [
'Martina JA',
' Diab HI',
' Lishu L',
' Jeong-A L',
' Patange S',
' Raben N',
' Puertollano R.',
],
journal: 'Science signaling',
year: 2014,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
pubmedId: '24448649',
citationCount: 321,
},
type: 'PUBMED',
resource: '24448649',
id: 154973,
annotatorClassName: 'source1',
},
{
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
article: {
title:
'Transcription factor EB: from master coordinator of lysosomal pathways to candidate therapeutic target in degenerative storage diseases.',
authors: ['Sardiello M.'],
journal: 'Annals of the New York Academy of Sciences',
year: 2016,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/27299292',
pubmedId: '27299292',
citationCount: 66,
},
type: 'PUBMED',
resource: '27299292',
id: 154974,
annotatorClassName: 'source2',
},
];
export const REFERENCES_MOCK_ALL_INVALID: Reference[] = [
{
link: null,
article: {
title:
'The nutrient-responsive transcription factor TFE3 promotes autophagy, lysosomal biogenesis, and clearance of cellular debris.',
authors: [
'Martina JA',
' Diab HI',
' Lishu L',
' Jeong-A L',
' Patange S',
' Raben N',
' Puertollano R.',
],
journal: 'Science signaling',
year: 2014,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
pubmedId: '24448649',
citationCount: 321,
},
type: 'PUBMED',
resource: '24448649',
id: 154973,
annotatorClassName: '',
},
{
link: null,
article: {
title:
'Transcription factor EB: from master coordinator of lysosomal pathways to candidate therapeutic target in degenerative storage diseases.',
authors: ['Sardiello M.'],
journal: 'Annals of the New York Academy of Sciences',
year: 2016,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/27299292',
pubmedId: '27299292',
citationCount: 66,
},
type: 'PUBMED',
resource: '27299292',
id: 154974,
annotatorClassName: '',
},
{
link: null,
article: {
title:
'The nutrient-responsive transcription factor TFE3 promotes autophagy, lysosomal biogenesis, and clearance of cellular debris.',
authors: [
'Martina JA',
' Diab HI',
' Lishu L',
' Jeong-A L',
' Patange S',
' Raben N',
' Puertollano R.',
],
journal: 'Science signaling',
year: 2014,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/24448649',
pubmedId: '24448649',
citationCount: 321,
},
type: 'PUBMED',
resource: '24448649',
id: 154973,
annotatorClassName: 'source1',
},
{
link: null,
article: {
title:
'Transcription factor EB: from master coordinator of lysosomal pathways to candidate therapeutic target in degenerative storage diseases.',
authors: ['Sardiello M.'],
journal: 'Annals of the New York Academy of Sciences',
year: 2016,
link: 'https://www.ncbi.nlm.nih.gov/pubmed/27299292',
pubmedId: '27299292',
citationCount: 66,
},
type: 'PUBMED',
resource: '27299292',
id: 154974,
annotatorClassName: 'source2',
},
];
export const SINGLE_VALID_REFERENCE = REFERENCES_MOCK_ALL_VALID[FIRST_ARRAY_ELEMENT];
......@@ -2,7 +2,7 @@ import { z } from 'zod';
import { articleSchema } from './articleSchema';
export const referenceSchema = z.object({
link: z.string().nullable(),
link: z.string().url().nullable(),
article: articleSchema.optional(),
type: z.string(),
resource: z.string(),
......