Skip to content

Commit e06980b

Browse files
committed
feat: implement client details
1 parent f8d7f30 commit e06980b

File tree

20 files changed

+293
-204
lines changed

20 files changed

+293
-204
lines changed

apps/api/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
"private": true,
66
"license": "Apache-2.0",
77
"scripts": {
8+
"_dev": "NODE_ENV=development env-cmd -f ../../.env node --enable-source-maps --import @swc-node/register/esm-register --watch src/main.ts",
9+
"_dev:test": "NODE_ENV=test env-cmd -f ../../.env node --enable-source-maps --import @swc-node/register/esm-register --watch src/main.ts",
810
"build": "NODE_ENV=production tsx scripts/build.ts",
911
"db:generate": "prisma generate",
10-
"dev": "NODE_ENV=development env-cmd -f ../../.env node --enable-source-maps --import @swc-node/register/esm-register --watch src/main.ts",
11-
"dev:test": "NODE_ENV=test env-cmd -f ../../.env node --enable-source-maps --import @swc-node/register/esm-register --watch src/main.ts",
12+
"dev": "NODE_ENV=development env-cmd -f ../../.env tsx scripts/dev.ts",
13+
"dev:test": "NODE_ENV=test env-cmd -f ../../.env tsx scripts/dev.ts",
1214
"format": "prettier --write src",
1315
"lint": "tsc && eslint --fix src",
1416
"start": "NODE_ENV=production env-cmd -f ../../.env node dist/app.js",

apps/api/scripts/build.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const options: BuildOptions & { external: NonNullable<unknown>; plugins: NonNull
3030
},
3131
bundle: true,
3232
define: {
33-
'import.meta.release': JSON.stringify(await getReleaseInfo())
33+
__RELEASE__: JSON.stringify(await getReleaseInfo())
3434
},
3535
entryPoints: [entryFile],
3636
external: ['@nestjs/microservices', '@nestjs/websockets/socket-module', 'class-transformer', 'class-validator'],
@@ -70,4 +70,39 @@ async function build() {
7070
console.log('Done!');
7171
}
7272

73-
await build();
73+
async function watch() {
74+
return new Promise((resolve, reject) => {
75+
esbuild
76+
.context({
77+
...options,
78+
external: [...options.external, 'esbuild'],
79+
plugins: [
80+
...options.plugins,
81+
{
82+
name: 'rebuild',
83+
setup(build) {
84+
build.onEnd((result) => {
85+
console.log(`Done! Build completed with ${result.errors.length} errors`);
86+
resolve(result);
87+
});
88+
}
89+
}
90+
],
91+
sourcemap: true
92+
})
93+
.then((ctx) => {
94+
void ctx.watch();
95+
console.log('Watching...');
96+
})
97+
.catch((err) => {
98+
reject(err as Error);
99+
});
100+
});
101+
}
102+
103+
const isEntry = process.argv[1] === import.meta.filename;
104+
if (isEntry) {
105+
await build();
106+
}
107+
108+
export { clean, outfile, watch };

apps/api/scripts/dev.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env tsx
2+
3+
import nodemon from 'nodemon';
4+
5+
import { clean, outfile, watch } from './build.js';
6+
7+
await clean();
8+
await watch();
9+
nodemon({
10+
script: outfile
11+
});

apps/api/src/instruments/instruments.service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { CryptoService } from '@douglasneuroinformatics/libnest/crypto';
2+
import { LoggingService } from '@douglasneuroinformatics/libnest/logging';
23
import { Injectable } from '@nestjs/common';
34
import {
45
ConflictException,
@@ -40,6 +41,7 @@ export class InstrumentsService {
4041
constructor(
4142
@InjectModel('Instrument') private readonly instrumentModel: Model<'Instrument'>,
4243
private readonly cryptoService: CryptoService,
44+
private readonly loggingService: LoggingService,
4345
private readonly virtualizationService: VirtualizationService
4446
) {}
4547

@@ -55,6 +57,7 @@ export class InstrumentsService {
5557
try {
5658
bundleReturn = await this.virtualizationService.runInContext(bundle);
5759
} catch (err) {
60+
this.loggingService.error(err);
5861
let cause: unknown;
5962
const parsed = await $Error.safeParseAsync(err);
6063
if (parsed.success) {

apps/outreach/src/plugins/starlight-plugin-typedoc/theme.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -120,16 +120,8 @@ class StarlightTypeDocThemeRenderContext extends MarkdownThemeContext {
120120
return `${markdown}\n\n${getAsideMarkdown(...args)}`;
121121
}
122122

123-
#addDeprecatedAside(markdown: string, blockTag: CommentTag) {
124-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
125-
const content =
126-
blockTag.content.length > 0
127-
? // @ts-expect-error - inherited code from https://github.com/HiDeoo/starlight-typedoc
128-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
129-
this.partials.commentParts(blockTag.content)
130-
: 'This API is no longer supported and may be removed in a future release.';
131-
132-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
123+
#addDeprecatedAside(markdown: string, _blockTag: CommentTag) {
124+
const content = 'This API is no longer supported and may be removed in a future release.';
133125
return this.#addAside(markdown, 'caution', 'Deprecated', content);
134126
}
135127

apps/web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"@opendatacapture/instrument-renderer": "workspace:*",
2929
"@opendatacapture/instrument-stubs": "workspace:*",
3030
"@opendatacapture/instrument-utils": "workspace:*",
31+
"@opendatacapture/licenses": "workspace:*",
3132
"@opendatacapture/react-core": "workspace:*",
3233
"@opendatacapture/runtime-core": "workspace:*",
3334
"@opendatacapture/runtime-v1": "workspace:*",

apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.stories.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export default { component: InstrumentCard } as Meta<typeof InstrumentCard>;
1010

1111
export const Default: Story = {
1212
args: {
13-
instrument: unilingualFormInstrument.instance,
13+
instrument: { ...unilingualFormInstrument.instance, supportedLanguages: [] },
1414
onClick: () => alert('Click!')
1515
}
1616
};

apps/web/src/features/instruments/components/InstrumentCard/InstrumentCard.tsx

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
1-
import { Card } from '@douglasneuroinformatics/libui/components';
1+
import { Card, Heading, Tooltip } from '@douglasneuroinformatics/libui/components';
22
import { useTranslation } from '@douglasneuroinformatics/libui/hooks';
3+
import type { Language } from '@douglasneuroinformatics/libui/i18n';
4+
import { licenses } from '@opendatacapture/licenses';
35
import { InstrumentIcon } from '@opendatacapture/react-core';
46
import type { UnilingualInstrumentInfo } from '@opendatacapture/schemas/instrument';
7+
import { BadgeAlertIcon, BadgeCheckIcon } from 'lucide-react';
58

69
export type InstrumentCardProps = {
7-
instrument: UnilingualInstrumentInfo;
10+
instrument: UnilingualInstrumentInfo & {
11+
supportedLanguages: Language[];
12+
};
813
onClick: () => void;
914
};
1015

1116
export const InstrumentCard = ({ instrument, onClick }: InstrumentCardProps) => {
1217
const { t } = useTranslation();
1318

19+
const license = licenses.get(instrument.details.license);
20+
1421
return (
1522
<Card
1623
className="relative flex flex-col p-8 transition-all duration-300 ease-in-out hover:scale-[1.03] hover:cursor-pointer sm:flex-row"
@@ -23,15 +30,106 @@ export const InstrumentCard = ({ instrument, onClick }: InstrumentCardProps) =>
2330
<InstrumentIcon className="rounded-full" kind={instrument.kind} style={{ height: 'auto', width: '32px' }} />
2431
</div>
2532
<div className="flex-grow">
26-
<h3
33+
<Heading
2734
className="title-font mb-0.5 font-semibold text-slate-900 dark:text-slate-100"
2835
data-cy="instrument-card-title"
36+
variant="h4"
2937
>
3038
{instrument.details.title}
31-
</h3>
32-
<h5 className="mb-2 text-sm text-slate-700 dark:text-slate-300" data-cy="instrument-card-tags">
33-
{`${t('core.tags')}: ${instrument.tags.join(', ')}`}
34-
</h5>
39+
</Heading>
40+
<div className="text-muted-foreground mb-2 flex flex-col text-sm">
41+
{instrument.details.authors && (
42+
<span>{`${t({ en: 'Authors:', fr: 'Auteurs :' })} ${instrument.details.authors.join(', ')}`}</span>
43+
)}
44+
<span data-cy="instrument-card-tags">
45+
{`${t({ en: 'Tags:', fr: 'Tags :' })} ${instrument.tags.join(', ')}`}
46+
</span>
47+
<span>
48+
{`${t({
49+
en: 'Supported Languages: ',
50+
fr: 'Langues disponibles :'
51+
})} ${instrument.supportedLanguages
52+
.map((language) => {
53+
switch (language) {
54+
case 'en':
55+
return 'English';
56+
case 'fr':
57+
return 'Français';
58+
}
59+
})
60+
.join(', ')}`}
61+
</span>
62+
<div className="flex items-center">
63+
<span className="text-muted-foreground text-sm">
64+
{t({
65+
en: 'License: ',
66+
fr: 'Licence : '
67+
}) + (license?.name ?? 'NA')}
68+
</span>
69+
&nbsp;
70+
<Tooltip>
71+
<Tooltip.Trigger className="p-1" size="icon" variant="ghost">
72+
{license?.isOpenSource ? (
73+
<BadgeCheckIcon className="fill-green-600 stroke-white" />
74+
) : (
75+
<BadgeAlertIcon className="fill-red-600 stroke-white" />
76+
)}
77+
</Tooltip.Trigger>
78+
<Tooltip.Content>
79+
<p>
80+
{license?.isOpenSource
81+
? t({
82+
en: 'This is a free and open-source license',
83+
fr: "Il s'agit d'une licence libre"
84+
})
85+
: t({
86+
en: 'This is not a free and open source license',
87+
fr: "Il ne s'agit pas d'une licence libre"
88+
})}
89+
</p>
90+
</Tooltip.Content>
91+
</Tooltip>
92+
</div>
93+
{instrument.details.referenceUrl && (
94+
<div className="flex items-center">
95+
<span>
96+
{t({
97+
en: 'Reference Link: ',
98+
fr: 'Lien vers la référence : '
99+
})}
100+
</span>
101+
&nbsp;
102+
<a
103+
className="text-muted-foreground hover:text-foreground text-sm underline underline-offset-1"
104+
href={instrument.details.referenceUrl}
105+
rel="noopener noreferrer"
106+
target="_blank"
107+
>
108+
{instrument.details.referenceUrl}
109+
</a>
110+
</div>
111+
)}
112+
{instrument.details.sourceUrl && (
113+
<div className="flex items-center">
114+
<span>
115+
{t({
116+
en: 'Source Link',
117+
fr: 'Lien vers le code source'
118+
})}
119+
</span>
120+
&nbsp;
121+
<a
122+
className="text-muted-foreground hover:text-foreground text-sm underline underline-offset-1"
123+
href={instrument.details.sourceUrl}
124+
rel="noopener noreferrer"
125+
target="_blank"
126+
>
127+
{instrument.details.sourceUrl}
128+
</a>
129+
</div>
130+
)}
131+
</div>
132+
<div className="flex items-center"></div>
35133
<p className="text-sm leading-relaxed text-slate-700 dark:text-slate-300">{instrument.details.description}</p>
36134
</div>
37135
</Card>

apps/web/src/features/instruments/components/InstrumentShowcase/InstrumentShowcase.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useEffect, useState } from 'react';
33
import { ListboxDropdown, SearchBar } from '@douglasneuroinformatics/libui/components';
44
import type { ListboxDropdownOption } from '@douglasneuroinformatics/libui/components';
55
import { useTranslation } from '@douglasneuroinformatics/libui/hooks';
6+
import type { Language } from '@douglasneuroinformatics/libui/i18n';
67
import type { UnilingualInstrumentInfo } from '@opendatacapture/schemas/instrument';
78
import { motion } from 'framer-motion';
89
import { useNavigate } from 'react-router-dom';
@@ -17,7 +18,9 @@ export const InstrumentsShowcase = () => {
1718
const instrumentInfoQuery = useInstrumentInfoQuery();
1819
const navigate = useNavigate();
1920
const { t } = useTranslation();
20-
const [filteredInstruments, setFilteredInstruments] = useState<UnilingualInstrumentInfo[]>([]);
21+
const [filteredInstruments, setFilteredInstruments] = useState<
22+
(UnilingualInstrumentInfo & { supportedLanguages: Language[] })[]
23+
>([]);
2124
const [tagOptions, setTagOptions] = useState<ListboxDropdownOption[]>([]);
2225
const [selectedLanguages, setSelectedLanguages] = useState<ListboxDropdownOption[]>([]);
2326
const [selectedTags, setSelectedTags] = useState<ListboxDropdownOption[]>([]);

apps/web/src/hooks/useInstrumentInfoQuery.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@ export function useInstrumentInfoQuery<TKind extends InstrumentKind>({
2222
params
2323
});
2424
const infos = await $InstrumentInfo.array().parseAsync(response.data);
25-
return infos.map((instrument) => translateInstrumentInfo(instrument, resolvedLanguage ?? 'en'));
25+
return infos.map((instrument) => ({
26+
...translateInstrumentInfo(instrument, resolvedLanguage ?? 'en'),
27+
supportedLanguages: typeof instrument.language === 'string' ? [instrument.language] : instrument.language
28+
}));
2629
},
2730
queryKey: ['instrument-info', params?.kind, params?.subjectId, resolvedLanguage]
2831
});

0 commit comments

Comments
 (0)