Skip to content

Commit b979b0c

Browse files
authored
Merge pull request #174 from joshunrau/dev
fix issue with debounce and license filter rendering issue and mount uploads dir to host
2 parents 74f2d4f + 487f807 commit b979b0c

File tree

10 files changed

+120
-68
lines changed

10 files changed

+120
-68
lines changed

.env.template

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ VALKEY_VERSION=latest
4040
VALKEY_HOST=localhost
4141
VALKEY_PORT=6379
4242

43+
# Where to store uploaded datasets while processing. If not defined, defaults $PWD/uploads.
44+
UPLOADS_DIR=
45+
4346
## ---------------------------------
4447
## DEVELOPMENT
4548
## ---------------------------------

api/src/core/env.schema.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import fs from 'node:fs';
2+
13
import { $BooleanLike, $NumberLike, $UrlLike } from '@douglasneuroinformatics/libjs';
24
import { $BaseEnv } from '@douglasneuroinformatics/libnest';
35
import { z } from 'zod/v4';
4-
56
export const $Env = $BaseEnv
67
.omit({ API_PORT: true })
78
.extend({
@@ -16,6 +17,19 @@ export const $Env = $BaseEnv
1617
SMTP_PORT: $NumberLike.pipe(z.union([z.literal(25), z.literal(465), z.literal(587)])),
1718
SMTP_SECURE: $BooleanLike,
1819
SMTP_SENDER: z.string().min(1).email(),
20+
UPLOADS_DIR: z
21+
.string()
22+
.check((ctx) => {
23+
if (!fs.existsSync(ctx.value)) {
24+
ctx.issues.push({
25+
code: 'custom',
26+
input: ctx.value,
27+
message: 'Directory must exist',
28+
path: ['UPLOADS_DIR']
29+
});
30+
}
31+
})
32+
.optional(),
1933
VALIDATION_TIMEOUT: $NumberLike.pipe(z.number().positive().int()),
2034
VALKEY_HOST: z.string().min(1),
2135
VALKEY_PORT: $NumberLike.pipe(z.number().positive().int()),

api/src/datasets/datasets.service.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@databank/core';
1717
import type { $DatasetCardProps } from '@databank/core';
1818
import type { Model } from '@douglasneuroinformatics/libnest';
19-
import { InjectModel, InjectPrismaClient } from '@douglasneuroinformatics/libnest';
19+
import { ConfigService, InjectModel, InjectPrismaClient } from '@douglasneuroinformatics/libnest';
2020
import { InjectQueue } from '@nestjs/bullmq';
2121
import { Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
2222
import { PermissionLevel, PrismaClient } from '@prisma/client';
@@ -31,14 +31,19 @@ import { FileUploadQueueName } from './datasets.constants.js';
3131

3232
@Injectable()
3333
export class DatasetsService {
34+
private readonly uploadsDir: string;
35+
3436
constructor(
3537
@InjectModel('Dataset') private datasetModel: Model<'Dataset'>,
3638
@InjectPrismaClient() private prisma: PrismaClient,
39+
private readonly configService: ConfigService,
3740
private readonly usersService: UsersService,
3841
private readonly columnService: ColumnsService,
3942
private readonly tabularDataService: TabularDataService,
4043
@InjectQueue(FileUploadQueueName) private fileUploadQueue: Queue
41-
) {}
44+
) {
45+
this.uploadsDir = this.configService.get('UPLOADS_DIR') ?? path.resolve(process.cwd(), 'uploads');
46+
}
4247

4348
async addManager(datasetId: string, managerId: string, managerEmailToAdd: string) {
4449
const dataset = await this.canModifyDataset(datasetId, managerId);
@@ -178,11 +183,10 @@ export class DatasetsService {
178183
let dataset;
179184
if (typeof file !== 'string') {
180185
// Resolve once from configuration or env
181-
const uploadsDir = path.resolve(process.cwd(), 'uploads');
182-
await fs.promises.mkdir(uploadsDir, { recursive: true });
186+
await fs.promises.mkdir(this.uploadsDir, { recursive: true });
183187
// Generate a collision-free, sanitised filename
184188
const safeName = crypto.randomUUID() + path.extname(file.originalname);
185-
const filePath = path.join(uploadsDir, safeName);
189+
const filePath = path.join(this.uploadsDir, safeName);
186190
await fs.promises.writeFile(filePath, file.buffer);
187191
dataset = await this.datasetModel.create({
188192
data: {

docker-compose.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@ services:
2121
depends_on:
2222
- mongo
2323
- valkey
24+
volumes:
25+
- ./uploads:/app/uploads
2426
environment:
2527
- NODE_ENV=production
28+
- UPLOADS_DIR=/app/uploads
2629
- MONGO_URI=mongodb://mongo:27017
2730
- MONGO_REPLICA_SET=rs0
2831
- MONGO_RETRY_WRITES=true

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "databank",
33
"type": "module",
4-
"version": "0.0.1-alpha.8",
4+
"version": "0.0.1-alpha.9",
55
"private": true,
66
"packageManager": "pnpm@10.7.1",
77
"engines": {

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@douglasneuroinformatics/libjs": "catalog:",
2020
"@douglasneuroinformatics/liblicense": "^1.0.0",
2121
"@douglasneuroinformatics/libpasswd": "^0.0.3",
22-
"@douglasneuroinformatics/libui": "^4.6.1",
22+
"@douglasneuroinformatics/libui": "^4.8.0",
2323
"@headlessui/react": "^2.2.0",
2424
"@heroicons/react": "^2.2.0",
2525
"@tanstack/react-query": "^5.71.3",

web/src/features/dataset/pages/CreateDatasetPage.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable perfectionist/sort-objects */
22
import { useCallback, useState } from 'react';
33

4-
import { $DatasetLicenses, mostFrequentOpenSourceLicenses } from '@databank/core';
4+
import { $DatasetLicenses } from '@databank/core';
55
import { Button, Form, Heading } from '@douglasneuroinformatics/libui/components';
66
import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks';
77
import { useNavigate } from '@tanstack/react-router';
@@ -35,7 +35,7 @@ const CreateDatasetPage = () => {
3535
const [formData, setFormData] = useState<CreateDatasetFormData | null>(null);
3636
const [processingFile, setProcessingFile] = useState<boolean>(false);
3737
const [file, setFile] = useState<File | null>(null);
38-
const debouncedLicensesFilter = useDebounceLicensesFilter();
38+
const { licenseOptions, subscribe } = useDebounceLicensesFilter();
3939

4040
const createDataset = async () => {
4141
setProcessingFile(true);
@@ -148,23 +148,16 @@ const CreateDatasetPage = () => {
148148
variant: 'input'
149149
},
150150
license: {
151-
deps: ['searchLicenseString', 'isOpenSource'],
152-
kind: 'dynamic',
153-
render(data) {
154-
return {
155-
kind: 'string',
156-
label: 'Select License',
157-
options:
158-
debouncedLicensesFilter(data.searchLicenseString?.toLowerCase(), data.isOpenSource) ??
159-
mostFrequentOpenSourceLicenses,
160-
variant: 'select'
161-
};
162-
}
151+
kind: 'string',
152+
label: 'Select License',
153+
options: licenseOptions,
154+
variant: 'select'
163155
}
164156
}
165157
}
166158
]}
167159
submitBtnLabel="Confirm"
160+
subscribe={subscribe}
168161
validationSchema={$CreateDatasetFormValidation}
169162
onSubmit={(data) => {
170163
setFormData(data);

web/src/features/dataset/pages/EditDatasetInfoPage.tsx

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable perfectionist/sort-objects */
2-
import { $DatasetLicenses, $EditDatasetInfo, mostFrequentOpenSourceLicenses } from '@databank/core';
2+
import { $DatasetLicenses, $EditDatasetInfo } from '@databank/core';
33
import { Button, Form, Heading } from '@douglasneuroinformatics/libui/components';
44
import { useNotificationsStore, useTranslation } from '@douglasneuroinformatics/libui/hooks';
55
import { useNavigate, useParams } from '@tanstack/react-router';
@@ -24,7 +24,7 @@ const EditDatasetInfoPage = () => {
2424
const notifications = useNotificationsStore();
2525
const { t } = useTranslation('common');
2626

27-
const debouncedLicensesFilter = useDebounceLicensesFilter();
27+
const { subscribe, licenseOptions } = useDebounceLicensesFilter();
2828

2929
const permissionOption = {
3030
LOGIN: 'LOGIN',
@@ -99,24 +99,17 @@ const EditDatasetInfoPage = () => {
9999
variant: 'input'
100100
},
101101
license: {
102-
deps: ['searchLicenseString', 'isOpenSource'],
103-
kind: 'dynamic',
104-
render(data) {
105-
return {
106-
kind: 'string',
107-
label: 'Select License',
108-
options:
109-
debouncedLicensesFilter(data.searchLicenseString?.toLowerCase(), data.isOpenSource) ??
110-
mostFrequentOpenSourceLicenses,
111-
variant: 'select'
112-
};
113-
}
102+
kind: 'string',
103+
label: 'Select License',
104+
options: licenseOptions,
105+
variant: 'select'
114106
}
115107
},
116108
title: 'Dataset License'
117109
}
118110
]}
119111
resetBtn={true}
112+
subscribe={subscribe}
120113
validationSchema={$EditDatasetInfoDto}
121114
onSubmit={(data) => handleSubmit(data)}
122115
/>
Lines changed: 69 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,78 @@
1-
import { useCallback } from 'react';
1+
import { useMemo, useRef, useState } from 'react';
22

3-
import { licensesArrayLowercase, nonOpenSourceLicensesArray, openSourceLicensesArray } from '@databank/core';
4-
import type { LicenseWithLowercase } from '@databank/core';
5-
import { useDebounceCallback } from 'usehooks-ts';
3+
import {
4+
licensesArrayLowercase,
5+
mostFrequentOpenSourceLicenses,
6+
nonOpenSourceLicensesArray,
7+
openSourceLicensesArray
8+
} from '@databank/core';
9+
import type { $DatasetLicenses, LicenseWithLowercase } from '@databank/core';
10+
import type FormTypes from '@douglasneuroinformatics/libui-form-types';
11+
12+
type BaseFormData = {
13+
isOpenSource?: boolean;
14+
license?: $DatasetLicenses;
15+
searchLicenseString?: string;
16+
};
17+
18+
function toObject(licenses: typeof licensesArrayLowercase): { [key: string]: string } {
19+
return Object.fromEntries(
20+
licenses.map(([key, value]) => {
21+
return [key, value.name];
22+
})
23+
);
24+
}
625

726
export function useDebounceLicensesFilter() {
8-
const _filterLicenses = useCallback(
9-
(searchString: string | undefined, isOpenSource: boolean | undefined): { [key: string]: string } => {
10-
let filterLicensesArray: [string, LicenseWithLowercase][];
11-
if (isOpenSource === undefined) {
12-
filterLicensesArray = licensesArrayLowercase;
13-
} else {
14-
filterLicensesArray = isOpenSource ? openSourceLicensesArray : nonOpenSourceLicensesArray;
15-
}
27+
const timeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
28+
const [licenseOptions, setLicenseOptions] = useState<{ [key: string]: string }>(mostFrequentOpenSourceLicenses);
1629

17-
if (searchString !== undefined) {
18-
filterLicensesArray = filterLicensesArray.filter(
19-
([_, license]) =>
20-
license.lowercaseLicenseId.includes(searchString) || license.lowercaseName.includes(searchString)
21-
);
22-
}
30+
const subscribe = useMemo(
31+
() => ({
32+
onChange: (values: BaseFormData, setValues: React.Dispatch<React.SetStateAction<BaseFormData>>) => {
33+
clearTimeout(timeoutRef.current);
34+
const { isOpenSource, license, searchLicenseString } = values;
35+
36+
if (searchLicenseString === undefined && isOpenSource === undefined) {
37+
setLicenseOptions(mostFrequentOpenSourceLicenses);
38+
return;
39+
}
2340

24-
return Object.fromEntries(
25-
filterLicensesArray.map(([key, value]) => {
26-
return [key, value.name];
27-
})
28-
);
29-
},
41+
const searchString = searchLicenseString?.toLowerCase();
42+
43+
timeoutRef.current = setTimeout(() => {
44+
let filterLicensesArray: [string, LicenseWithLowercase][];
45+
if (isOpenSource === undefined) {
46+
filterLicensesArray = licensesArrayLowercase;
47+
} else {
48+
filterLicensesArray = isOpenSource ? openSourceLicensesArray : nonOpenSourceLicensesArray;
49+
}
50+
51+
if (!searchString) {
52+
setLicenseOptions(toObject(filterLicensesArray));
53+
if (filterLicensesArray.find(([key]) => key === license)) {
54+
setValues((prevValues) => ({ ...prevValues, license: undefined }));
55+
}
56+
return;
57+
}
58+
59+
filterLicensesArray = filterLicensesArray.filter(
60+
([_, license]) =>
61+
license.lowercaseLicenseId.includes(searchString) || license.lowercaseName.includes(searchString)
62+
);
63+
64+
setLicenseOptions(toObject(filterLicensesArray));
65+
if (filterLicensesArray.find(([key]) => key === license)) {
66+
setValues((prevValues) => ({ ...prevValues, license: undefined }));
67+
}
68+
}, 500);
69+
},
70+
selector: (values: FormTypes.PartialData<BaseFormData>) => {
71+
return `${values.isOpenSource}-${values.searchLicenseString}`;
72+
}
73+
}),
3074
[]
3175
);
3276

33-
const debouncedLicensesFilter = useDebounceCallback(_filterLicenses, 200);
34-
35-
return debouncedLicensesFilter;
77+
return { licenseOptions, subscribe };
3678
}

0 commit comments

Comments
 (0)