mirror of
https://github.com/soconnor0919/hristudio.git
synced 2026-09-23 04:20:16 -04:00
feat: add initial seed data migration and form builder components
- Created migration 0001_seed_data.sql to insert minimal seed data for users, accounts, and roles. - Added meta journal for migration tracking. - Implemented FormBuilder component for dynamic form field creation and management. - Developed FormFieldRenderer component to render various types of form fields based on user input. - Introduced constants for trust levels and status configurations. - Defined types for form fields and trial data structures to enhance type safety and clarity.
This commit is contained in:
@@ -4,18 +4,12 @@ import { signOut } from "~/lib/auth-client";
|
||||
import { toast } from "sonner";
|
||||
import { TRPCClientError } from "@trpc/client";
|
||||
|
||||
/**
|
||||
* Auth error codes that should trigger automatic logout
|
||||
*/
|
||||
const AUTH_ERROR_CODES = [
|
||||
"UNAUTHORIZED",
|
||||
"FORBIDDEN",
|
||||
"UNAUTHENTICATED",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Auth error messages that should trigger automatic logout
|
||||
*/
|
||||
const AUTH_ERROR_MESSAGES = [
|
||||
"unauthorized",
|
||||
"unauthenticated",
|
||||
@@ -27,15 +21,10 @@ const AUTH_ERROR_MESSAGES = [
|
||||
"access denied",
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* Checks if an error is an authentication/authorization error that should trigger logout
|
||||
*/
|
||||
export function isAuthError(error: unknown): boolean {
|
||||
if (!error) return false;
|
||||
|
||||
// Check TRPC errors
|
||||
if (error instanceof TRPCClientError) {
|
||||
// Check error code
|
||||
const trpcErrorData = error.data as
|
||||
| { code?: string; httpStatus?: number }
|
||||
| undefined;
|
||||
@@ -47,24 +36,20 @@ export function isAuthError(error: unknown): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check HTTP status codes
|
||||
const httpStatus = trpcErrorData?.httpStatus;
|
||||
if (httpStatus === 401 || httpStatus === 403) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check error message
|
||||
const message = error.message?.toLowerCase() ?? "";
|
||||
return AUTH_ERROR_MESSAGES.some((authMsg) => message.includes(authMsg));
|
||||
}
|
||||
|
||||
// Check generic errors
|
||||
if (error instanceof Error) {
|
||||
const message = error.message?.toLowerCase() || "";
|
||||
return AUTH_ERROR_MESSAGES.some((authMsg) => message.includes(authMsg));
|
||||
}
|
||||
|
||||
// Check error objects with message property
|
||||
if (typeof error === "object" && error !== null) {
|
||||
if ("message" in error) {
|
||||
const errorObj = error as { message: unknown };
|
||||
@@ -72,7 +57,6 @@ export function isAuthError(error: unknown): boolean {
|
||||
return AUTH_ERROR_MESSAGES.some((authMsg) => message.includes(authMsg));
|
||||
}
|
||||
|
||||
// Check for status codes in error objects
|
||||
if ("status" in error) {
|
||||
const statusObj = error as { status: unknown };
|
||||
const status = statusObj.status as number;
|
||||
@@ -83,9 +67,6 @@ export function isAuthError(error: unknown): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles authentication errors by logging out the user
|
||||
*/
|
||||
export async function handleAuthError(
|
||||
error: unknown,
|
||||
customMessage?: string,
|
||||
@@ -96,11 +77,9 @@ export async function handleAuthError(
|
||||
|
||||
console.warn("Authentication error detected, logging out user:", error);
|
||||
|
||||
// Show user-friendly message
|
||||
const message = customMessage ?? "Session expired. Please log in again.";
|
||||
toast.error(message);
|
||||
|
||||
// Small delay to let the toast show
|
||||
setTimeout(() => {
|
||||
void (async () => {
|
||||
try {
|
||||
@@ -108,72 +87,19 @@ export async function handleAuthError(
|
||||
window.location.href = "/";
|
||||
} catch (signOutError) {
|
||||
console.error("Error during sign out:", signOutError);
|
||||
// Force redirect if signOut fails
|
||||
window.location.href = "/";
|
||||
}
|
||||
})();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* React Query error handler that automatically handles auth errors
|
||||
*/
|
||||
export function createAuthErrorHandler(customMessage?: string) {
|
||||
return (error: unknown) => {
|
||||
void handleAuthError(error, customMessage);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* tRPC error handler that automatically handles auth errors
|
||||
*/
|
||||
export function handleTRPCError(error: unknown, customMessage?: string): void {
|
||||
void handleAuthError(error, customMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic error handler for any error type
|
||||
*/
|
||||
export function handleGenericError(
|
||||
error: unknown,
|
||||
customMessage?: string,
|
||||
): void {
|
||||
void handleAuthError(error, customMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook-style error handler for use in React components
|
||||
*/
|
||||
export function useAuthErrorHandler() {
|
||||
return {
|
||||
handleAuthError: (error: unknown, customMessage?: string) => {
|
||||
void handleAuthError(error, customMessage);
|
||||
},
|
||||
handleAuthError,
|
||||
isAuthError,
|
||||
createErrorHandler: createAuthErrorHandler,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Higher-order function to wrap API calls with automatic auth error handling
|
||||
*/
|
||||
export function withAuthErrorHandling<
|
||||
T extends (...args: unknown[]) => Promise<unknown>,
|
||||
>(fn: T, customMessage?: string): T {
|
||||
return (async (...args: Parameters<T>): Promise<ReturnType<T>> => {
|
||||
try {
|
||||
return (await fn(...args)) as ReturnType<T>;
|
||||
} catch (error) {
|
||||
await handleAuthError(error, customMessage);
|
||||
throw error; // Re-throw so calling code can handle it too
|
||||
}
|
||||
}) as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to check if current error should show a generic error message
|
||||
* (i.e., it's not an auth error that will auto-logout)
|
||||
*/
|
||||
export function shouldShowGenericError(error: unknown): boolean {
|
||||
return !isAuthError(error);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
export const trustLevelConfig = {
|
||||
official: {
|
||||
label: "Official",
|
||||
className: "bg-blue-100 text-blue-800 hover:bg-blue-200",
|
||||
description: "Official HRIStudio plugin",
|
||||
},
|
||||
verified: {
|
||||
label: "Verified",
|
||||
className: "bg-green-100 text-green-800 hover:bg-green-200",
|
||||
description: "Verified by the community",
|
||||
},
|
||||
community: {
|
||||
label: "Community",
|
||||
className: "bg-yellow-100 text-yellow-800 hover:bg-yellow-200",
|
||||
description: "Community contributed",
|
||||
},
|
||||
};
|
||||
|
||||
export const statusConfig = {
|
||||
active: {
|
||||
label: "Active",
|
||||
className: "bg-green-100 text-green-800 hover:bg-green-200",
|
||||
description: "Plugin is active and working",
|
||||
},
|
||||
deprecated: {
|
||||
label: "Deprecated",
|
||||
className: "bg-orange-100 text-orange-800 hover:bg-orange-200",
|
||||
description: "Plugin is deprecated",
|
||||
},
|
||||
inactive: {
|
||||
label: "Inactive",
|
||||
className: "bg-gray-100 text-gray-800 hover:bg-gray-200",
|
||||
description: "Plugin is not active",
|
||||
},
|
||||
};
|
||||
|
||||
export const formStatusColors = {
|
||||
pending: "bg-yellow-100 text-yellow-700",
|
||||
completed: "bg-green-100 text-green-700",
|
||||
rejected: "bg-red-100 text-red-700",
|
||||
};
|
||||
@@ -464,6 +464,7 @@ export class WizardRosService extends EventEmitter {
|
||||
* Subscribe to robot sensor topics
|
||||
*/
|
||||
private subscribeToRobotTopics(): void {
|
||||
console.log("[WizardROS] Setting up robot topics...");
|
||||
const topics = [
|
||||
{ topic: "/joint_states", type: "sensor_msgs/JointState" },
|
||||
{ topic: "/bumper", type: "naoqi_bridge_msgs/Bumper" },
|
||||
@@ -476,6 +477,11 @@ export class WizardRosService extends EventEmitter {
|
||||
topics.forEach(({ topic, type }) => {
|
||||
this.subscribe(topic, type);
|
||||
});
|
||||
|
||||
this.advertise("/speech", "std_msgs/String");
|
||||
this.advertise("/cmd_vel", "geometry_msgs/Twist");
|
||||
this.advertise("/robot_pose", "geometry_msgs/Pose");
|
||||
this.advertise("/animation", "std_msgs/String");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -492,6 +498,21 @@ export class WizardRosService extends EventEmitter {
|
||||
this.send(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Advertise a ROS topic (declare the type before publishing)
|
||||
*/
|
||||
private advertise(topic: string, messageType: string): void {
|
||||
console.log(`[WizardROS] Advertising topic ${topic} as ${messageType}`);
|
||||
const message: RosMessage = {
|
||||
op: "advertise",
|
||||
topic,
|
||||
type: messageType,
|
||||
id: `adv_${this.messageId++}`,
|
||||
};
|
||||
|
||||
this.send(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish message to ROS topic
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
export interface FormFieldSettings {
|
||||
scale?: number;
|
||||
}
|
||||
|
||||
export interface FormField {
|
||||
id: string;
|
||||
type: FormFieldType;
|
||||
label: string;
|
||||
required: boolean;
|
||||
options?: string[];
|
||||
settings?: FormFieldSettings;
|
||||
}
|
||||
|
||||
export type FormFieldType =
|
||||
| "text"
|
||||
| "textarea"
|
||||
| "multiple_choice"
|
||||
| "checkbox"
|
||||
| "rating"
|
||||
| "yes_no"
|
||||
| "date"
|
||||
| "signature";
|
||||
|
||||
export type FormType = "consent" | "survey" | "questionnaire";
|
||||
|
||||
export interface FormFieldTypeConfig {
|
||||
value: FormFieldType;
|
||||
label: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const FORM_FIELD_TYPES: FormFieldTypeConfig[] = [
|
||||
{ value: "text", label: "Text (short)", icon: "📝" },
|
||||
{ value: "textarea", label: "Text (long)", icon: "📄" },
|
||||
{ value: "multiple_choice", label: "Multiple Choice", icon: "☑️" },
|
||||
{ value: "checkbox", label: "Checkbox", icon: "✅" },
|
||||
{ value: "rating", label: "Rating Scale", icon: "⭐" },
|
||||
{ value: "yes_no", label: "Yes/No", icon: "✔️" },
|
||||
{ value: "date", label: "Date", icon: "📅" },
|
||||
{ value: "signature", label: "Signature", icon: "✍️" },
|
||||
];
|
||||
|
||||
export function createField(type: FormFieldType): FormField {
|
||||
return {
|
||||
id: crypto.randomUUID(),
|
||||
type,
|
||||
label: `New ${FORM_FIELD_TYPES.find((f) => f.value === type)?.label || "Field"}`,
|
||||
required: false,
|
||||
options: type === "multiple_choice" ? ["Option 1", "Option 2"] : undefined,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
export interface StepData {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
type: "wizard_action" | "robot_action" | "parallel_steps" | "conditional";
|
||||
parameters: Record<string, unknown>;
|
||||
conditions?: {
|
||||
options?: {
|
||||
label: string;
|
||||
value: string;
|
||||
nextStepId?: string;
|
||||
nextStepIndex?: number;
|
||||
variant?:
|
||||
| "default"
|
||||
| "destructive"
|
||||
| "outline"
|
||||
| "secondary"
|
||||
| "ghost"
|
||||
| "link";
|
||||
}[];
|
||||
};
|
||||
order: number;
|
||||
actions?: ActionData[];
|
||||
}
|
||||
|
||||
export interface ActionData {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
type: string;
|
||||
parameters: Record<string, unknown>;
|
||||
order: number;
|
||||
pluginId: string | null;
|
||||
}
|
||||
|
||||
export interface TrialData {
|
||||
id: string;
|
||||
status: TrialStatus;
|
||||
scheduledAt: Date | null;
|
||||
startedAt: Date | null;
|
||||
completedAt: Date | null;
|
||||
duration: number | null;
|
||||
sessionNumber: number | null;
|
||||
notes: string | null;
|
||||
experimentId: string;
|
||||
participantId: string | null;
|
||||
wizardId: string | null;
|
||||
experiment: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
studyId: string;
|
||||
};
|
||||
participant: {
|
||||
id: string;
|
||||
participantCode: string;
|
||||
demographics: Record<string, unknown> | null;
|
||||
};
|
||||
}
|
||||
|
||||
export type TrialStatus =
|
||||
| "scheduled"
|
||||
| "in_progress"
|
||||
| "completed"
|
||||
| "aborted"
|
||||
| "failed";
|
||||
|
||||
export interface TrialEvent {
|
||||
type: string;
|
||||
timestamp: Date;
|
||||
data?: unknown;
|
||||
message?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user