Use Base UI and clsx in form primitives
- Adopt Base UI for the shared form field, input, button, and toggle wrappers - Replace the local class-name helper with clsx to keep the primitives simpler - Keep native textarea and select controls where they still fit the existing styling pattern
This commit is contained in:
parent
f06ccd643e
commit
bd6be61b77
13 changed files with 266 additions and 128 deletions
2
dev.py
2
dev.py
|
|
@ -318,7 +318,7 @@ def watch_and_run_backend() -> None:
|
||||||
def main() -> int:
|
def main() -> int:
|
||||||
if not (ROOT_DIR / 'webui' / 'node_modules').is_dir():
|
if not (ROOT_DIR / 'webui' / 'node_modules').is_dir():
|
||||||
print('webui/node_modules is missing.')
|
print('webui/node_modules is missing.')
|
||||||
print('Run: cd webui && npm install')
|
print('Run: cd webui && npm ci')
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
vite_proc = start_vite()
|
vite_proc = start_vite()
|
||||||
|
|
|
||||||
120
webui/package-lock.json
generated
120
webui/package-lock.json
generated
|
|
@ -6,9 +6,11 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "soulsync-webui",
|
"name": "soulsync-webui",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.4.1",
|
||||||
"@tanstack/react-form": "^1.29.1",
|
"@tanstack/react-form": "^1.29.1",
|
||||||
"@tanstack/react-query": "^5.100.5",
|
"@tanstack/react-query": "^5.100.5",
|
||||||
"@tanstack/react-router": "^1.168.24",
|
"@tanstack/react-router": "^1.168.24",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"ky": "^2.0.2",
|
"ky": "^2.0.2",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5"
|
"react-dom": "^19.2.5"
|
||||||
|
|
@ -338,7 +340,6 @@
|
||||||
"version": "7.29.2",
|
"version": "7.29.2",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
|
||||||
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
"integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
|
|
@ -392,6 +393,66 @@
|
||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@base-ui/react": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@base-ui/react/-/react-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.2",
|
||||||
|
"@base-ui/utils": "0.2.8",
|
||||||
|
"@floating-ui/react-dom": "^2.1.8",
|
||||||
|
"@floating-ui/utils": "^0.2.11",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/mui-org"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@date-fns/tz": "^1.2.0",
|
||||||
|
"@types/react": "^17 || ^18 || ^19",
|
||||||
|
"date-fns": "^4.0.0",
|
||||||
|
"react": "^17 || ^18 || ^19",
|
||||||
|
"react-dom": "^17 || ^18 || ^19"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@date-fns/tz": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"date-fns": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@base-ui/utils": {
|
||||||
|
"version": "0.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.2.8.tgz",
|
||||||
|
"integrity": "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.29.2",
|
||||||
|
"@floating-ui/utils": "^0.2.11",
|
||||||
|
"reselect": "^5.1.1",
|
||||||
|
"use-sync-external-store": "^1.6.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^17 || ^18 || ^19",
|
||||||
|
"react": "^17 || ^18 || ^19",
|
||||||
|
"react-dom": "^17 || ^18 || ^19"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@bramus/specificity": {
|
"node_modules/@bramus/specificity": {
|
||||||
"version": "2.4.2",
|
"version": "2.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||||
|
|
@ -563,6 +624,44 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||||
|
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.5",
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.8.tgz",
|
||||||
|
"integrity": "sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.7.6"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||||
|
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@inquirer/ansi": {
|
"node_modules/@inquirer/ansi": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.5.tgz",
|
||||||
|
|
@ -2001,7 +2100,7 @@
|
||||||
"version": "19.2.14",
|
"version": "19.2.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
|
|
@ -2631,6 +2730,15 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|
@ -2703,7 +2811,7 @@
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-urls": {
|
"node_modules/data-urls": {
|
||||||
|
|
@ -3952,6 +4060,12 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/rettime": {
|
"node_modules/rettime": {
|
||||||
"version": "0.11.8",
|
"version": "0.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,11 @@
|
||||||
"test:e2e": "playwright test"
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@base-ui/react": "^1.4.1",
|
||||||
"@tanstack/react-form": "^1.29.1",
|
"@tanstack/react-form": "^1.29.1",
|
||||||
"@tanstack/react-query": "^5.100.5",
|
"@tanstack/react-query": "^5.100.5",
|
||||||
"@tanstack/react-router": "^1.168.24",
|
"@tanstack/react-router": "^1.168.24",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
"ky": "^2.0.2",
|
"ky": "^2.0.2",
|
||||||
"react": "^19.2.5",
|
"react": "^19.2.5",
|
||||||
"react-dom": "^19.2.5"
|
"react-dom": "^19.2.5"
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
import { describe, expect, it, vi } from 'vite-plus/test';
|
import type { ResponsePromise } from 'ky';
|
||||||
|
|
||||||
import { HTTPError } from 'ky';
|
import { HTTPError } from 'ky';
|
||||||
|
import { describe, expect, it, vi } from 'vite-plus/test';
|
||||||
import type { ResponsePromise } from 'ky';
|
|
||||||
|
|
||||||
import { readJson } from './api-client';
|
import { readJson } from './api-client';
|
||||||
|
|
||||||
|
|
@ -46,9 +45,6 @@ describe('readJson', () => {
|
||||||
const result = readJson(promise);
|
const result = readJson(promise);
|
||||||
|
|
||||||
await expect(result).rejects.toBe(error);
|
await expect(result).rejects.toBe(error);
|
||||||
await expect(result).rejects.toHaveProperty(
|
await expect(result).rejects.toHaveProperty('message', error.message);
|
||||||
'message',
|
|
||||||
error.message,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
|
import { Button as BaseButton } from '@base-ui/react/button';
|
||||||
|
import { Field } from '@base-ui/react/field';
|
||||||
|
import { Input as BaseInput } from '@base-ui/react/input';
|
||||||
|
import { Toggle as BaseToggle } from '@base-ui/react/toggle';
|
||||||
|
import clsx from 'clsx';
|
||||||
import {
|
import {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
type ComponentPropsWithoutRef,
|
||||||
type ButtonHTMLAttributes,
|
type ButtonHTMLAttributes,
|
||||||
type InputHTMLAttributes,
|
|
||||||
type SelectHTMLAttributes,
|
type SelectHTMLAttributes,
|
||||||
type ReactNode,
|
type ReactNode,
|
||||||
type TextareaHTMLAttributes,
|
type TextareaHTMLAttributes,
|
||||||
|
|
@ -9,10 +14,6 @@ import {
|
||||||
|
|
||||||
import styles from './form.module.css';
|
import styles from './form.module.css';
|
||||||
|
|
||||||
function mergeClassNames(...classNames: Array<string | false | null | undefined>) {
|
|
||||||
return classNames.filter(Boolean).join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FormFieldProps {
|
export interface FormFieldProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
@ -31,30 +32,36 @@ export function FormField({
|
||||||
label,
|
label,
|
||||||
}: FormFieldProps) {
|
}: FormFieldProps) {
|
||||||
return (
|
return (
|
||||||
<div className={mergeClassNames(styles.field, className)}>
|
<Field.Root className={clsx(styles.field, className)}>
|
||||||
<div className={styles.fieldHeader}>
|
<div className={styles.fieldHeader}>
|
||||||
{htmlFor ? (
|
{htmlFor ? (
|
||||||
<label className={styles.fieldLabel} htmlFor={htmlFor}>
|
<label className={styles.fieldLabel} htmlFor={htmlFor}>
|
||||||
{label}
|
{label}
|
||||||
</label>
|
</label>
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.fieldLabel}>{label}</div>
|
<Field.Label className={styles.fieldLabel}>{label}</Field.Label>
|
||||||
)}
|
)}
|
||||||
{helperText ? <div className={styles.fieldHelper}>{helperText}</div> : null}
|
{helperText ? (
|
||||||
|
<Field.Description className={styles.fieldHelper}>{helperText}</Field.Description>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.fieldControl}>{children}</div>
|
<div className={styles.fieldControl}>{children}</div>
|
||||||
{error ? <FormError message={error} /> : null}
|
{error ? <FormError message={error} /> : null}
|
||||||
</div>
|
</Field.Root>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextInputProps = InputHTMLAttributes<HTMLInputElement>;
|
type BaseInputProps = ComponentPropsWithoutRef<typeof BaseInput>;
|
||||||
|
|
||||||
|
export type TextInputProps = Omit<BaseInputProps, 'className'> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
|
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(function TextInput(
|
||||||
{ className, ...props },
|
{ className, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return <input ref={ref} className={mergeClassNames(styles.textInput, className)} {...props} />;
|
return <BaseInput ref={ref} className={clsx(styles.textInput, className)} {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
|
export type TextAreaProps = TextareaHTMLAttributes<HTMLTextAreaElement>;
|
||||||
|
|
@ -63,7 +70,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
|
||||||
{ className, ...props },
|
{ className, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return <textarea ref={ref} className={mergeClassNames(styles.textArea, className)} {...props} />;
|
return <textarea ref={ref} className={clsx(styles.textArea, className)} {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
export type SelectProps = SelectHTMLAttributes<HTMLSelectElement>;
|
export type SelectProps = SelectHTMLAttributes<HTMLSelectElement>;
|
||||||
|
|
@ -72,7 +79,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(function Select
|
||||||
{ className, ...props },
|
{ className, ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return <select ref={ref} className={mergeClassNames(styles.select, className)} {...props} />;
|
return <select ref={ref} className={clsx(styles.select, className)} {...props} />;
|
||||||
});
|
});
|
||||||
|
|
||||||
export function OptionCardGroup({
|
export function OptionCardGroup({
|
||||||
|
|
@ -82,14 +89,19 @@ export function OptionCardGroup({
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return <div className={mergeClassNames(styles.optionCardGroup, className)}>{children}</div>;
|
return <div className={clsx(styles.optionCardGroup, className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OptionCardProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {
|
export interface OptionCardProps extends Omit<
|
||||||
|
ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
'title' | 'value'
|
||||||
|
> {
|
||||||
|
className?: string;
|
||||||
description?: ReactNode;
|
description?: ReactNode;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionCard = forwardRef<HTMLButtonElement, OptionCardProps>(function OptionCard(
|
export const OptionCard = forwardRef<HTMLButtonElement, OptionCardProps>(function OptionCard(
|
||||||
|
|
@ -97,14 +109,10 @@ export const OptionCard = forwardRef<HTMLButtonElement, OptionCardProps>(functio
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<button
|
<BaseToggle
|
||||||
ref={ref}
|
ref={ref}
|
||||||
aria-pressed={selected}
|
pressed={selected}
|
||||||
className={mergeClassNames(
|
className={clsx(styles.optionCard, selected && styles.optionCardSelected, className)}
|
||||||
styles.optionCard,
|
|
||||||
selected && styles.optionCardSelected,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
type={type}
|
type={type}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|
@ -117,7 +125,7 @@ export const OptionCard = forwardRef<HTMLButtonElement, OptionCardProps>(functio
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</BaseToggle>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -128,11 +136,13 @@ export function OptionButtonGroup({
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return <div className={mergeClassNames(styles.optionButtonGroup, className)}>{children}</div>;
|
return <div className={clsx(styles.optionButtonGroup, className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OptionButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface OptionButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'value'> {
|
||||||
|
className?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
value?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OptionButton = forwardRef<HTMLButtonElement, OptionButtonProps>(function OptionButton(
|
export const OptionButton = forwardRef<HTMLButtonElement, OptionButtonProps>(function OptionButton(
|
||||||
|
|
@ -140,48 +150,41 @@ export const OptionButton = forwardRef<HTMLButtonElement, OptionButtonProps>(fun
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<button
|
<BaseToggle
|
||||||
ref={ref}
|
ref={ref}
|
||||||
aria-pressed={selected}
|
pressed={selected}
|
||||||
className={mergeClassNames(
|
className={clsx(styles.optionButton, selected && styles.optionButtonSelected, className)}
|
||||||
styles.optionButton,
|
|
||||||
selected && styles.optionButtonSelected,
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
type={type}
|
type={type}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</BaseToggle>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
|
type BaseButtonProps = ComponentPropsWithoutRef<typeof BaseButton>;
|
||||||
|
|
||||||
|
export type ButtonProps = Omit<BaseButtonProps, 'className'> & {
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||||
{ className, type = 'button', ...props },
|
{ className, type = 'button', ...props },
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
return (
|
return <BaseButton ref={ref} className={clsx(styles.button, className)} type={type} {...props} />;
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
className={mergeClassNames(styles.button, className)}
|
|
||||||
type={type}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export function FormError({ className, message }: { className?: string; message?: ReactNode }) {
|
export function FormError({ className, message }: { className?: string; message?: ReactNode }) {
|
||||||
if (!message) return null;
|
if (!message) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={mergeClassNames(styles.formError, className)} role="alert">
|
<div className={clsx(styles.formError, className)} role="alert">
|
||||||
{message}
|
{message}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormActions({ className, children }: { children: ReactNode; className?: string }) {
|
export function FormActions({ className, children }: { children: ReactNode; className?: string }) {
|
||||||
return <div className={mergeClassNames(styles.formActions, className)}>{children}</div>;
|
return <div className={clsx(styles.formActions, className)}>{children}</div>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { queryOptions } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { apiClient, readJson } from '@/app/api-client';
|
import { apiClient, readJson } from '@/app/api-client';
|
||||||
|
|
||||||
|
import type { NormalizedIssuesSearch } from './-issues.helpers';
|
||||||
import type {
|
import type {
|
||||||
CreateIssuePayload,
|
CreateIssuePayload,
|
||||||
IssueCounts,
|
IssueCounts,
|
||||||
|
|
@ -11,8 +12,6 @@ import type {
|
||||||
IssueRecord,
|
IssueRecord,
|
||||||
} from './-issues.types';
|
} from './-issues.types';
|
||||||
|
|
||||||
import type { NormalizedIssuesSearch } from './-issues.helpers';
|
|
||||||
|
|
||||||
const DEFAULT_LIMIT = 100;
|
const DEFAULT_LIMIT = 100;
|
||||||
|
|
||||||
function createIssueHeaders(profileId: number, extra?: HeadersInit): Headers {
|
function createIssueHeaders(profileId: number, extra?: HeadersInit): Headers {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,4 @@
|
||||||
import type {
|
import type { IssueRecord, IssuesSearch, IssueSnapshot } from './-issues.types';
|
||||||
IssueRecord,
|
|
||||||
IssuesSearch,
|
|
||||||
IssueSnapshot,
|
|
||||||
} from './-issues.types';
|
|
||||||
|
|
||||||
export const REFRESH_EVENT = 'ss:issues-refresh';
|
export const REFRESH_EVENT = 'ss:issues-refresh';
|
||||||
export const CLOSE_EVENT = 'ss:issues-close-detail';
|
export const CLOSE_EVENT = 'ss:issues-close-detail';
|
||||||
|
|
@ -116,15 +112,13 @@ export function normalizeIssuesSearch(search: IssuesSearch | undefined): Normali
|
||||||
status === 'dismissed'
|
status === 'dismissed'
|
||||||
? status
|
? status
|
||||||
: DEFAULT_ISSUES_SEARCH.status,
|
: DEFAULT_ISSUES_SEARCH.status,
|
||||||
category: typeof category === 'string' && ISSUE_CATEGORY_KEYS.has(category)
|
category: typeof category === 'string' && ISSUE_CATEGORY_KEYS.has(category) ? category : 'all',
|
||||||
? category
|
|
||||||
: 'all',
|
|
||||||
issueId:
|
issueId:
|
||||||
(typeof issueId === 'number' && Number.isInteger(issueId) && issueId > 0)
|
typeof issueId === 'number' && Number.isInteger(issueId) && issueId > 0
|
||||||
? issueId
|
? issueId
|
||||||
: (typeof issueId === 'string' && /^[1-9]\d*$/.test(issueId)
|
: typeof issueId === 'string' && /^[1-9]\d*$/.test(issueId)
|
||||||
? Number(issueId)
|
? Number(issueId)
|
||||||
: undefined),
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ export interface IssueSnapshot {
|
||||||
track_deezer_id?: string;
|
track_deezer_id?: string;
|
||||||
artist_tidal_id?: string;
|
artist_tidal_id?: string;
|
||||||
album_tidal_id?: string;
|
album_tidal_id?: string;
|
||||||
|
artist_qobuz_id?: string | number;
|
||||||
|
album_qobuz_id?: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IssueRecord {
|
export interface IssueRecord {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,13 @@ describe('issues route', () => {
|
||||||
if (url.includes('/api/issues/counts')) {
|
if (url.includes('/api/issues/counts')) {
|
||||||
return createResponse({
|
return createResponse({
|
||||||
success: true,
|
success: true,
|
||||||
counts: { open: 2, in_progress: 1, resolved: 0, dismissed: 0, total: 3 },
|
counts: {
|
||||||
|
open: 2,
|
||||||
|
in_progress: 1,
|
||||||
|
resolved: 0,
|
||||||
|
dismissed: 0,
|
||||||
|
total: 3,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (url.includes('/api/issues?')) {
|
if (url.includes('/api/issues?')) {
|
||||||
|
|
@ -105,18 +111,18 @@ describe('issues route', () => {
|
||||||
status: 'open',
|
status: 'open',
|
||||||
priority: 'normal',
|
priority: 'normal',
|
||||||
snapshot_data: {
|
snapshot_data: {
|
||||||
title: 'Album Name',
|
title: 'Album Name',
|
||||||
artist_name: 'Artist',
|
artist_name: 'Artist',
|
||||||
thumb_url: 'https://example.com/thumb.jpg',
|
thumb_url: 'https://example.com/thumb.jpg',
|
||||||
spotify_album_id: 'abc123',
|
spotify_album_id: 'abc123',
|
||||||
track_number: 1,
|
track_number: 1,
|
||||||
duration: 245,
|
duration: 245,
|
||||||
format: 'FLAC',
|
format: 'FLAC',
|
||||||
bitrate: 1411,
|
bitrate: 1411,
|
||||||
},
|
|
||||||
created_at: '2026-04-03 10:30:00',
|
|
||||||
reporter_name: 'Ada',
|
|
||||||
},
|
},
|
||||||
|
created_at: '2026-04-03 10:30:00',
|
||||||
|
reporter_name: 'Ada',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (url.includes('/api/spotify/album/abc123')) {
|
if (url.includes('/api/spotify/album/abc123')) {
|
||||||
|
|
@ -151,6 +157,10 @@ describe('issues route', () => {
|
||||||
it('loads the detail modal from the route search state', async () => {
|
it('loads the detail modal from the route search state', async () => {
|
||||||
renderIssuesRoute(['/issues?issueId=7']);
|
renderIssuesRoute(['/issues?issueId=7']);
|
||||||
await waitFor(() => expect(screen.getByRole('dialog')).toHaveTextContent('Issue #7'));
|
await waitFor(() => expect(screen.getByRole('dialog')).toHaveTextContent('Issue #7'));
|
||||||
|
expect(await screen.findByTitle('Spotify Album')).toHaveAttribute(
|
||||||
|
'href',
|
||||||
|
'https://open.spotify.com/album/abc123',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stores filters in route search state', async () => {
|
it('stores filters in route search state', async () => {
|
||||||
|
|
@ -186,7 +196,9 @@ describe('issues route', () => {
|
||||||
renderIssuesRoute();
|
renderIssuesRoute();
|
||||||
fireEvent.click(await screen.findByTestId('issue-card-7'));
|
fireEvent.click(await screen.findByTestId('issue-card-7'));
|
||||||
|
|
||||||
const closeButton = await screen.findByRole('button', { name: /close issue detail/i });
|
const closeButton = await screen.findByRole('button', {
|
||||||
|
name: /close issue detail/i,
|
||||||
|
});
|
||||||
const deleteButton = await screen.findByRole('button', { name: /delete/i });
|
const deleteButton = await screen.findByRole('button', { name: /delete/i });
|
||||||
|
|
||||||
deleteButton.focus();
|
deleteButton.focus();
|
||||||
|
|
@ -240,7 +252,9 @@ describe('issues route', () => {
|
||||||
|
|
||||||
fireEvent.change(titleInput, { target: { value: 'Custom report title' } });
|
fireEvent.change(titleInput, { target: { value: 'Custom report title' } });
|
||||||
fireEvent.blur(titleInput);
|
fireEvent.blur(titleInput);
|
||||||
fireEvent.change(descriptionInput, { target: { value: 'Detailed reproduction notes' } });
|
fireEvent.change(descriptionInput, {
|
||||||
|
target: { value: 'Detailed reproduction notes' },
|
||||||
|
});
|
||||||
fireEvent.click(screen.getByRole('button', { name: /high/i }));
|
fireEvent.click(screen.getByRole('button', { name: /high/i }));
|
||||||
fireEvent.click(screen.getByRole('button', { name: /wrong metadata/i }));
|
fireEvent.click(screen.getByRole('button', { name: /wrong metadata/i }));
|
||||||
expect(titleInput).toHaveValue('Custom report title');
|
expect(titleInput).toHaveValue('Custom report title');
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
|
|
||||||
import type { IssueRecord } from '../-issues.types';
|
import type { IssueRecord } from '../-issues.types';
|
||||||
|
|
||||||
|
import { deleteIssue, updateIssue } from '../-issues.api';
|
||||||
import {
|
import {
|
||||||
formatIssueDate,
|
formatIssueDate,
|
||||||
formatStatusLabel,
|
formatStatusLabel,
|
||||||
|
|
@ -17,7 +18,6 @@ import {
|
||||||
ISSUE_CATEGORY_META,
|
ISSUE_CATEGORY_META,
|
||||||
parseSnapshot,
|
parseSnapshot,
|
||||||
} from '../-issues.helpers';
|
} from '../-issues.helpers';
|
||||||
import { deleteIssue, updateIssue } from '../-issues.api';
|
|
||||||
import styles from './issue-detail-modal.module.css';
|
import styles from './issue-detail-modal.module.css';
|
||||||
|
|
||||||
export function IssueDetailModal({
|
export function IssueDetailModal({
|
||||||
|
|
@ -51,9 +51,8 @@ export function IssueDetailModal({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
previouslyFocusedElementRef.current = document.activeElement instanceof HTMLElement
|
previouslyFocusedElementRef.current =
|
||||||
? document.activeElement
|
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
||||||
: null;
|
|
||||||
|
|
||||||
const focusModal = () => {
|
const focusModal = () => {
|
||||||
const modal = modalRef.current;
|
const modal = modalRef.current;
|
||||||
|
|
@ -213,7 +212,11 @@ export function IssueDetailModal({
|
||||||
className={styles.modalButtonReopen}
|
className={styles.modalButtonReopen}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
updateMutation.mutate({ issueId: issue.id, status: 'open', adminResponse })
|
updateMutation.mutate({
|
||||||
|
issueId: issue.id,
|
||||||
|
status: 'open',
|
||||||
|
adminResponse,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
disabled={updateMutation.isPending}
|
disabled={updateMutation.isPending}
|
||||||
>
|
>
|
||||||
|
|
@ -381,7 +384,9 @@ export function IssueDetailModal({
|
||||||
|
|
||||||
<div className={styles.issueDetailInfoBar}>
|
<div className={styles.issueDetailInfoBar}>
|
||||||
<div className={styles.issueDetailInfoLeft}>
|
<div className={styles.issueDetailInfoLeft}>
|
||||||
<span className={`${styles.issuePriorityDot} ${getPriorityDotClassName(priorityClassName)}`} />
|
<span
|
||||||
|
className={`${styles.issuePriorityDot} ${getPriorityDotClassName(priorityClassName)}`}
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
className={`${styles.issueStatusBadge} ${getStatusClassName(issue.status)}`}
|
className={`${styles.issueStatusBadge} ${getStatusClassName(issue.status)}`}
|
||||||
>
|
>
|
||||||
|
|
@ -465,11 +470,12 @@ export function IssueDetailModal({
|
||||||
{trackRows.length > 0 ? (
|
{trackRows.length > 0 ? (
|
||||||
<div className={styles.issueDetailSection}>
|
<div className={styles.issueDetailSection}>
|
||||||
<div className={styles.issueDetailSectionTitle}>
|
<div className={styles.issueDetailSectionTitle}>
|
||||||
Track Listing <span className={styles.issueDetailSectionCount}>{trackRows.length} tracks</span>
|
Track Listing{' '}
|
||||||
</div>
|
<span className={styles.issueDetailSectionCount}>
|
||||||
<div className={styles.issueDetailTracklist}>
|
{trackRows.length} tracks
|
||||||
{renderTrackListing(trackRows)}
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div className={styles.issueDetailTracklist}>{renderTrackListing(trackRows)}</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
@ -564,13 +570,12 @@ function renderTrackListing(trackRows: Array<Record<string, unknown>>) {
|
||||||
const formatClassName = getTrackFormatClassName(format);
|
const formatClassName = getTrackFormatClassName(format);
|
||||||
const bitrateClassName = getTrackBitrateClassName(bitrateValue, format);
|
const bitrateClassName = getTrackBitrateClassName(bitrateValue, format);
|
||||||
nodes.push(
|
nodes.push(
|
||||||
<div className={styles.issueDetailTracklistRow} key={String(track.id || `${track.title}-${index}`)}>
|
<div
|
||||||
<span className={styles.issueDetailTracklistNum}>
|
className={styles.issueDetailTracklistRow}
|
||||||
{String(track.track_number || '-')}
|
key={String(track.id || `${track.title}-${index}`)}
|
||||||
</span>
|
>
|
||||||
<span className={styles.issueDetailTracklistTitle}>
|
<span className={styles.issueDetailTracklistNum}>{String(track.track_number || '-')}</span>
|
||||||
{String(track.title || 'Unknown')}
|
<span className={styles.issueDetailTracklistTitle}>{String(track.title || 'Unknown')}</span>
|
||||||
</span>
|
|
||||||
<span className={styles.issueDetailTracklistDur}>{duration}</span>
|
<span className={styles.issueDetailTracklistDur}>{duration}</span>
|
||||||
<span className={styles.issueDetailTracklistMeta}>
|
<span className={styles.issueDetailTracklistMeta}>
|
||||||
{format ? (
|
{format ? (
|
||||||
|
|
@ -630,21 +635,19 @@ function formatDuration(value: unknown): string {
|
||||||
}
|
}
|
||||||
|
|
||||||
function getExternalLinks(snapshot: ReturnType<typeof parseSnapshot>) {
|
function getExternalLinks(snapshot: ReturnType<typeof parseSnapshot>) {
|
||||||
const links: Array<
|
const links: Array<{
|
||||||
| {
|
className:
|
||||||
className:
|
| 'issueExternalLinkSpotify'
|
||||||
| 'issueExternalLinkSpotify'
|
| 'issueExternalLinkMusicBrainz'
|
||||||
| 'issueExternalLinkMusicBrainz'
|
| 'issueExternalLinkDeezer'
|
||||||
| 'issueExternalLinkDeezer'
|
| 'issueExternalLinkTidal'
|
||||||
| 'issueExternalLinkTidal'
|
| 'issueExternalLinkQobuz';
|
||||||
| 'issueExternalLinkQobuz';
|
id?: string | number;
|
||||||
id?: string | number;
|
label: string;
|
||||||
label: string;
|
service: string;
|
||||||
service: string;
|
type: string;
|
||||||
type: string;
|
url?: string;
|
||||||
url?: string;
|
}> = [];
|
||||||
}
|
|
||||||
> = [];
|
|
||||||
if (snapshot.spotify_artist_id) {
|
if (snapshot.spotify_artist_id) {
|
||||||
links.push({
|
links.push({
|
||||||
className: 'issueExternalLinkSpotify',
|
className: 'issueExternalLinkSpotify',
|
||||||
|
|
@ -789,15 +792,27 @@ function getAlbumMetaParts(
|
||||||
function getTrackMetaItems(snapshot: ReturnType<typeof parseSnapshot>) {
|
function getTrackMetaItems(snapshot: ReturnType<typeof parseSnapshot>) {
|
||||||
const items: Array<{ icon: string; label: string; value: string }> = [];
|
const items: Array<{ icon: string; label: string; value: string }> = [];
|
||||||
if (snapshot.track_number) {
|
if (snapshot.track_number) {
|
||||||
items.push({ icon: '#', label: 'Track', value: String(snapshot.track_number) });
|
items.push({
|
||||||
|
icon: '#',
|
||||||
|
label: 'Track',
|
||||||
|
value: String(snapshot.track_number),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const duration = formatDuration(snapshot.duration);
|
const duration = formatDuration(snapshot.duration);
|
||||||
if (duration) items.push({ icon: 'T', label: 'Duration', value: duration });
|
if (duration) items.push({ icon: 'T', label: 'Duration', value: duration });
|
||||||
if (snapshot.format) items.push({ icon: 'F', label: 'Format', value: String(snapshot.format) });
|
if (snapshot.format) items.push({ icon: 'F', label: 'Format', value: String(snapshot.format) });
|
||||||
if (snapshot.bitrate)
|
if (snapshot.bitrate)
|
||||||
items.push({ icon: 'B', label: 'Bitrate', value: `${snapshot.bitrate} kbps` });
|
items.push({
|
||||||
|
icon: 'B',
|
||||||
|
label: 'Bitrate',
|
||||||
|
value: `${snapshot.bitrate} kbps`,
|
||||||
|
});
|
||||||
if (snapshot.bpm) items.push({ icon: 'M', label: 'BPM', value: String(snapshot.bpm) });
|
if (snapshot.bpm) items.push({ icon: 'M', label: 'BPM', value: String(snapshot.bpm) });
|
||||||
if (snapshot.quality)
|
if (snapshot.quality)
|
||||||
items.push({ icon: 'Q', label: 'Quality', value: String(snapshot.quality) });
|
items.push({
|
||||||
|
icon: 'Q',
|
||||||
|
label: 'Quality',
|
||||||
|
value: String(snapshot.quality),
|
||||||
|
});
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,15 +20,12 @@ import { useShellBridge } from '@/platform/shell/route-controllers';
|
||||||
|
|
||||||
import type { IssuePriority, IssueReportPayload } from '../-issues.types';
|
import type { IssuePriority, IssueReportPayload } from '../-issues.types';
|
||||||
|
|
||||||
|
import { createIssue, issueCountsQueryOptions } from '../-issues.api';
|
||||||
import {
|
import {
|
||||||
REFRESH_EVENT,
|
REFRESH_EVENT,
|
||||||
createDefaultIssueTitle,
|
createDefaultIssueTitle,
|
||||||
getIssueCategoriesForEntity,
|
getIssueCategoriesForEntity,
|
||||||
} from '../-issues.helpers';
|
} from '../-issues.helpers';
|
||||||
import {
|
|
||||||
createIssue,
|
|
||||||
issueCountsQueryOptions,
|
|
||||||
} from '../-issues.api';
|
|
||||||
import styles from './issue-detail-modal.module.css';
|
import styles from './issue-detail-modal.module.css';
|
||||||
|
|
||||||
const ISSUE_DOMAIN_QUERY_KEY = ['issues'] as const;
|
const ISSUE_DOMAIN_QUERY_KEY = ['issues'] as const;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,11 @@ import { useReactPageShell } from '@/platform/shell/route-controllers';
|
||||||
|
|
||||||
import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types';
|
import type { IssueCounts, IssueRecord, IssueStatus } from '../-issues.types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
issueCountsQueryOptions,
|
||||||
|
issueDetailQueryOptions,
|
||||||
|
issueListQueryOptions,
|
||||||
|
} from '../-issues.api';
|
||||||
import {
|
import {
|
||||||
CLOSE_EVENT,
|
CLOSE_EVENT,
|
||||||
REFRESH_EVENT,
|
REFRESH_EVENT,
|
||||||
|
|
@ -23,11 +28,6 @@ import {
|
||||||
normalizeIssuesSearch,
|
normalizeIssuesSearch,
|
||||||
parseSnapshot,
|
parseSnapshot,
|
||||||
} from '../-issues.helpers';
|
} from '../-issues.helpers';
|
||||||
import {
|
|
||||||
issueCountsQueryOptions,
|
|
||||||
issueDetailQueryOptions,
|
|
||||||
issueListQueryOptions,
|
|
||||||
} from '../-issues.api';
|
|
||||||
import { Route } from '../route';
|
import { Route } from '../route';
|
||||||
import { IssueDetailModal } from './issue-detail-modal';
|
import { IssueDetailModal } from './issue-detail-modal';
|
||||||
import styles from './issues-page.module.css';
|
import styles from './issues-page.module.css';
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,9 @@ export const Route = createFileRoute('/issues')({
|
||||||
context.queryClient.ensureQueryData(issueCountsQueryOptions(profile.profileId)),
|
context.queryClient.ensureQueryData(issueCountsQueryOptions(profile.profileId)),
|
||||||
context.queryClient.ensureQueryData(issueListQueryOptions(profile.profileId, deps)),
|
context.queryClient.ensureQueryData(issueListQueryOptions(profile.profileId, deps)),
|
||||||
deps.issueId
|
deps.issueId
|
||||||
? context.queryClient.ensureQueryData(issueDetailQueryOptions(profile.profileId, deps.issueId))
|
? context.queryClient.ensureQueryData(
|
||||||
|
issueDetailQueryOptions(profile.profileId, deps.issueId),
|
||||||
|
)
|
||||||
: Promise.resolve(),
|
: Promise.resolve(),
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue