mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Support updating problems
This commit is contained in:
parent
0d3c3eb471
commit
a5e53a023b
4
.github/workflows/playwright.yaml
vendored
4
.github/workflows/playwright.yaml
vendored
@ -50,6 +50,8 @@ jobs:
|
||||
env:
|
||||
SUPABASE_TEST_USER_EMAIL: ${{ secrets.SUPABASE_TEST_USER_EMAIL }}
|
||||
SUPABASE_TEST_USER_PASSWORD: ${{ secrets.SUPABASE_TEST_USER_PASSWORD }}
|
||||
NUT_LOGIN_KEY: ${{ secrets.NUT_LOGIN_KEY }}
|
||||
NUT_PASSWORD: ${{ secrets.NUT_PASSWORD }}
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
@ -99,6 +101,8 @@ jobs:
|
||||
PLAYWRIGHT_TEST_BASE_URL: ${{ steps.deploy.outputs.WITH_SUPABASE_PREVIEW_URL }}
|
||||
SUPABASE_TEST_USER_EMAIL: ${{ secrets.SUPABASE_TEST_USER_EMAIL }}
|
||||
SUPABASE_TEST_USER_PASSWORD: ${{ secrets.SUPABASE_TEST_USER_PASSWORD }}
|
||||
NUT_LOGIN_KEY: ${{ secrets.NUT_LOGIN_KEY }}
|
||||
NUT_PASSWORD: ${{ secrets.NUT_PASSWORD }}
|
||||
|
||||
- name: Upload test results
|
||||
if: always()
|
||||
|
@ -118,7 +118,7 @@ export const SettingsWindow = ({ open, onClose }: SettingsProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<RadixDialog.Close asChild onClick={onClose}>
|
||||
<IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
|
||||
<IconButton testId="dialog-close" icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
|
||||
</RadixDialog.Close>
|
||||
</motion.div>
|
||||
</RadixDialog.Content>
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { useState, useCallback } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import Cookies from 'js-cookie';
|
||||
import { anthropicNumFreeUsesCookieName, anthropicApiKeyCookieName, maxFreeUses } from '~/utils/freeUses';
|
||||
import { saveNutLoginKey, saveProblemsUsername, getNutLoginKey, getProblemsUsername } from '~/lib/replay/Problems';
|
||||
import { saveNutLoginKey, saveUsername, getNutLoginKey, getUsername } from '~/lib/replay/Problems';
|
||||
import { debounce } from '~/utils/debounce';
|
||||
|
||||
export default function ConnectionsTab() {
|
||||
const [apiKey, setApiKey] = useState(Cookies.get(anthropicApiKeyCookieName) || '');
|
||||
const [username, setUsername] = useState(getProblemsUsername() || '');
|
||||
const [username, setUsername] = useState(getUsername() || '');
|
||||
const [loginKey, setLoginKey] = useState(getNutLoginKey() || '');
|
||||
const numFreeUses = +(Cookies.get(anthropicNumFreeUsesCookieName) || 0);
|
||||
|
||||
@ -20,9 +21,16 @@ export default function ConnectionsTab() {
|
||||
setApiKey(key);
|
||||
};
|
||||
|
||||
const saveUsernameWithToast = (username: string) => {
|
||||
saveUsername(username);
|
||||
toast.success('Username saved!');
|
||||
};
|
||||
|
||||
const debouncedSaveUsername = useCallback(debounce(saveUsernameWithToast, 1000), []);
|
||||
|
||||
const handleSaveUsername = async (username: string) => {
|
||||
saveProblemsUsername(username);
|
||||
setUsername(username);
|
||||
debouncedSaveUsername(username);
|
||||
};
|
||||
|
||||
const handleSaveLoginKey = async (key: string) => {
|
||||
@ -61,6 +69,7 @@ export default function ConnectionsTab() {
|
||||
<div className="flex-1 mr-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter your username"
|
||||
value={username}
|
||||
onChange={(e) => handleSaveUsername(e.target.value)}
|
||||
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
|
||||
@ -72,6 +81,7 @@ export default function ConnectionsTab() {
|
||||
<div className="flex-1 mr-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter your login key"
|
||||
value={loginKey}
|
||||
onChange={(e) => handleSaveLoginKey(e.target.value)}
|
||||
className="w-full bg-white dark:bg-bolt-elements-background-depth-4 relative px-2 py-1.5 rounded-md focus:outline-none placeholder-bolt-elements-textTertiary text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary border border-bolt-elements-borderColor disabled:opacity-50"
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { motion, type Variants } from 'framer-motion';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { toast } from 'react-toastify';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Dialog, DialogButton, DialogDescription, DialogRoot, DialogTitle } from '~/components/ui/Dialog';
|
||||
import { ThemeSwitch } from '~/components/ui/ThemeSwitch';
|
||||
import { SettingsWindow } from '~/components/settings/SettingsWindow';
|
||||
@ -13,7 +14,7 @@ import { binDates } from './date-binning';
|
||||
import { useSearchFilter } from '~/lib/hooks/useSearchFilter';
|
||||
import { SaveProblem } from './SaveProblem';
|
||||
import { SaveReproductionModal } from './SaveReproduction';
|
||||
import { getNutIsAdmin } from '~/lib/replay/Problems';
|
||||
import { isAdminStore } from '~/lib/stores/user';
|
||||
|
||||
const menuVariants = {
|
||||
closed: {
|
||||
@ -46,6 +47,7 @@ export const Menu = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [dialogContent, setDialogContent] = useState<DialogContent>(null);
|
||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||
const isAdmin = useStore(isAdminStore);
|
||||
|
||||
const { filteredItems: filteredList, handleSearchChange } = useSearchFilter({
|
||||
items: list,
|
||||
@ -140,7 +142,7 @@ export const Menu = () => {
|
||||
Problems
|
||||
</a>
|
||||
<SaveProblem />
|
||||
{getNutIsAdmin() && <SaveReproductionModal />}
|
||||
{isAdmin && <SaveReproductionModal />}
|
||||
<a
|
||||
href="/about"
|
||||
className="flex gap-2 bg-bolt-elements-sidebar-buttonBackgroundDefault text-bolt-elements-sidebar-buttonText hover:bg-bolt-elements-sidebar-buttonBackgroundHover rounded-md p-2 transition-theme"
|
||||
|
@ -2,7 +2,7 @@ import { toast } from 'react-toastify';
|
||||
import ReactModal from 'react-modal';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { workbenchStore } from '~/lib/stores/workbench';
|
||||
import { getProblemsUsername, submitProblem, saveProblemsUsername, BoltProblemStatus } from '~/lib/replay/Problems';
|
||||
import { getUsername, submitProblem, saveUsername, BoltProblemStatus } from '~/lib/replay/Problems';
|
||||
import type { BoltProblemInput } from '~/lib/replay/Problems';
|
||||
import { getRepositoryContents } from '~/lib/replay/Repository';
|
||||
import { shouldUseSupabase, getCurrentUser, isAuthenticated } from '~/lib/supabase/client';
|
||||
@ -31,7 +31,7 @@ export function SaveProblem() {
|
||||
} else {
|
||||
setIsLoggedIn(true); // Always considered logged in when not using Supabase
|
||||
|
||||
const username = getProblemsUsername();
|
||||
const username = getUsername();
|
||||
|
||||
if (username) {
|
||||
setFormData((prev) => ({ ...prev, username }));
|
||||
@ -70,7 +70,7 @@ export function SaveProblem() {
|
||||
|
||||
// Only save username to cookie if not using Supabase
|
||||
if (!shouldUseSupabase()) {
|
||||
saveProblemsUsername(formData.username);
|
||||
saveUsername(formData.username);
|
||||
}
|
||||
|
||||
toast.info('Submitting problem...');
|
||||
|
@ -124,7 +124,7 @@ export const Dialog = memo(({ className, children, onBackdrop, onClose }: Dialog
|
||||
>
|
||||
{children}
|
||||
<RadixDialog.Close asChild onClick={onClose}>
|
||||
<IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
|
||||
<IconButton testId="dialog-close" icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
|
||||
</RadixDialog.Close>
|
||||
</motion.div>
|
||||
</RadixDialog.Content>
|
||||
|
@ -17,11 +17,13 @@ interface BaseIconButtonProps {
|
||||
type IconButtonWithoutChildrenProps = {
|
||||
icon: string;
|
||||
children?: undefined;
|
||||
testId?: string;
|
||||
} & BaseIconButtonProps;
|
||||
|
||||
type IconButtonWithChildrenProps = {
|
||||
icon?: undefined;
|
||||
children: string | JSX.Element | JSX.Element[];
|
||||
testId?: string;
|
||||
} & BaseIconButtonProps;
|
||||
|
||||
type IconButtonProps = IconButtonWithoutChildrenProps | IconButtonWithChildrenProps;
|
||||
@ -37,6 +39,7 @@ export const IconButton = memo(
|
||||
iconClassName,
|
||||
disabledClassName,
|
||||
disabled = false,
|
||||
testId,
|
||||
title,
|
||||
onClick,
|
||||
children,
|
||||
@ -56,6 +59,7 @@ export const IconButton = memo(
|
||||
)}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
data-testid={testId || 'icon-button'}
|
||||
onClick={(event) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
|
@ -13,6 +13,8 @@ import {
|
||||
supabaseSubmitFeedback,
|
||||
supabaseDeleteProblem,
|
||||
} from '~/lib/supabase/problems';
|
||||
import { getNutIsAdmin as getNutIsAdminFromSupabase } from '~/lib/supabase/client';
|
||||
import { updateIsAdmin, updateUsername } from '~/lib/stores/user';
|
||||
|
||||
export interface BoltProblemComment {
|
||||
username?: string;
|
||||
@ -59,10 +61,11 @@ export interface BoltProblem extends BoltProblemDescription {
|
||||
export type BoltProblemInput = Omit<BoltProblem, 'problemId' | 'timestamp'>;
|
||||
|
||||
export async function listAllProblems(): Promise<BoltProblemDescription[]> {
|
||||
if (shouldUseSupabase()) {
|
||||
return supabaseListAllProblems();
|
||||
}
|
||||
let problems: BoltProblemDescription[] = [];
|
||||
|
||||
if (shouldUseSupabase()) {
|
||||
problems = await supabaseListAllProblems();
|
||||
} else {
|
||||
try {
|
||||
const rv = await sendCommandDedicatedClient({
|
||||
method: 'Recording.globalExperimentalCommand',
|
||||
@ -72,13 +75,27 @@ export async function listAllProblems(): Promise<BoltProblemDescription[]> {
|
||||
});
|
||||
console.log('ListProblemsRval', rv);
|
||||
|
||||
return (rv as any).rval.problems.reverse();
|
||||
problems = (rv as any).rval.problems.reverse();
|
||||
|
||||
const filteredProblems = problems.filter((problem) => {
|
||||
// if ?showAll=true is not in the url, filter out [test] problems
|
||||
if (window.location.search.includes('showAll=true')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !problem.title.includes('[test]');
|
||||
});
|
||||
|
||||
return filteredProblems;
|
||||
} catch (error) {
|
||||
console.error('Error fetching problems', error);
|
||||
toast.error('Failed to fetch problems');
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
return problems;
|
||||
}
|
||||
|
||||
export async function getProblem(problemId: string): Promise<BoltProblem | null> {
|
||||
@ -192,13 +209,18 @@ export async function updateProblem(problemId: string, problem: BoltProblemInput
|
||||
|
||||
const nutLoginKeyCookieName = 'nutLoginKey';
|
||||
const nutIsAdminCookieName = 'nutIsAdmin';
|
||||
const nutUsernameCookieName = 'nutUsername';
|
||||
|
||||
export function getNutLoginKey(): string | undefined {
|
||||
const cookieValue = Cookies.get(nutLoginKeyCookieName);
|
||||
return cookieValue?.length ? cookieValue : undefined;
|
||||
}
|
||||
|
||||
export function getNutIsAdmin(): boolean {
|
||||
export async function getNutIsAdmin(): Promise<boolean> {
|
||||
if (shouldUseSupabase()) {
|
||||
return getNutIsAdminFromSupabase();
|
||||
}
|
||||
|
||||
return Cookies.get(nutIsAdminCookieName) === 'true';
|
||||
}
|
||||
|
||||
@ -222,22 +244,26 @@ export async function saveNutLoginKey(key: string) {
|
||||
console.log('UserInfo', userInfo);
|
||||
|
||||
Cookies.set(nutLoginKeyCookieName, key);
|
||||
Cookies.set(nutIsAdminCookieName, userInfo.admin ? 'true' : 'false');
|
||||
setNutIsAdmin(userInfo.admin);
|
||||
}
|
||||
|
||||
export function setNutIsAdmin(isAdmin: boolean) {
|
||||
Cookies.set(nutIsAdminCookieName, isAdmin ? 'true' : 'false');
|
||||
|
||||
// Update the store
|
||||
updateIsAdmin(isAdmin);
|
||||
}
|
||||
|
||||
const nutProblemsUsernameCookieName = 'nutProblemsUsername';
|
||||
|
||||
export function getProblemsUsername(): string | undefined {
|
||||
const cookieValue = Cookies.get(nutProblemsUsernameCookieName);
|
||||
export function getUsername(): string | undefined {
|
||||
const cookieValue = Cookies.get(nutUsernameCookieName);
|
||||
return cookieValue?.length ? cookieValue : undefined;
|
||||
}
|
||||
|
||||
export function saveProblemsUsername(username: string) {
|
||||
Cookies.set(nutProblemsUsernameCookieName, username);
|
||||
export function saveUsername(username: string) {
|
||||
Cookies.set(nutUsernameCookieName, username);
|
||||
|
||||
// Update the store
|
||||
updateUsername(username);
|
||||
}
|
||||
|
||||
export async function submitFeedback(feedback: any): Promise<boolean> {
|
||||
|
54
app/lib/stores/user.ts
Normal file
54
app/lib/stores/user.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { atom } from 'nanostores';
|
||||
import { getNutIsAdmin, getUsername } from '~/lib/replay/Problems';
|
||||
import { userStore } from './auth';
|
||||
|
||||
// Store for admin status
|
||||
export const isAdminStore = atom<boolean>(false);
|
||||
|
||||
// Store for username
|
||||
export const usernameStore = atom<string | undefined>(undefined);
|
||||
|
||||
// Safe store updaters that check for browser environment
|
||||
export function updateIsAdmin(value: boolean) {
|
||||
if (typeof window !== 'undefined') {
|
||||
isAdminStore.set(value);
|
||||
}
|
||||
}
|
||||
|
||||
export function updateUsername(username: string | undefined) {
|
||||
if (typeof window !== 'undefined') {
|
||||
usernameStore.set(username);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the user stores
|
||||
export async function initializeUserStores() {
|
||||
try {
|
||||
// Only run in browser environment
|
||||
if (typeof window === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Initialize with current values
|
||||
const isAdmin = await getNutIsAdmin();
|
||||
isAdminStore.set(isAdmin);
|
||||
|
||||
const username = getUsername();
|
||||
usernameStore.set(username);
|
||||
|
||||
// Subscribe to user changes to update admin status
|
||||
return userStore.subscribe(async (user) => {
|
||||
if (user) {
|
||||
// When user changes, recalculate admin status
|
||||
const isAdmin = await getNutIsAdmin();
|
||||
isAdminStore.set(isAdmin);
|
||||
} else {
|
||||
// Reset when logged out
|
||||
isAdminStore.set(false);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize user stores', error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
@ -54,6 +54,9 @@ export interface Database {
|
||||
let supabaseUrl = '';
|
||||
let supabaseAnonKey = '';
|
||||
|
||||
// Add a singleton client instance
|
||||
let supabaseClientInstance: ReturnType<typeof createClient<Database>> | null = null;
|
||||
|
||||
/**
|
||||
* Determines whether Supabase should be used based on URL parameters and environment variables.
|
||||
* URL parameters take precedence over environment variables.
|
||||
@ -86,6 +89,27 @@ export async function getCurrentUser(): Promise<SupabaseUser | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function getNutIsAdmin(): Promise<boolean> {
|
||||
const user = await getCurrentUser();
|
||||
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { data: profileData, error: profileError } = await getSupabase()
|
||||
.from('profiles')
|
||||
.select('is_admin')
|
||||
.eq('id', user?.id)
|
||||
.single();
|
||||
|
||||
if (profileError) {
|
||||
console.error('Error fetching user profile:', profileError);
|
||||
return false;
|
||||
}
|
||||
|
||||
return profileData?.is_admin || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if there is a currently authenticated user.
|
||||
*/
|
||||
@ -95,6 +119,11 @@ export async function isAuthenticated(): Promise<boolean> {
|
||||
}
|
||||
|
||||
export function getSupabase() {
|
||||
// If we already have an instance, return it
|
||||
if (supabaseClientInstance) {
|
||||
return supabaseClientInstance;
|
||||
}
|
||||
|
||||
// Determine execution environment and get appropriate variables
|
||||
if (typeof window == 'object') {
|
||||
supabaseUrl = window.ENV.SUPABASE_URL || '';
|
||||
@ -115,13 +144,15 @@ export function getSupabase() {
|
||||
console.warn('Missing Supabase environment variables. Some features may not work properly.');
|
||||
}
|
||||
|
||||
// Create and return the Supabase client
|
||||
return createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
||||
// Create and cache the Supabase client
|
||||
supabaseClientInstance = createClient<Database>(supabaseUrl, supabaseAnonKey, {
|
||||
auth: {
|
||||
persistSession: true,
|
||||
autoRefreshToken: true,
|
||||
},
|
||||
});
|
||||
|
||||
return supabaseClientInstance;
|
||||
}
|
||||
|
||||
// Helper function to check if Supabase is properly initialized
|
||||
|
@ -3,7 +3,7 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import { getSupabase, type Database } from './client';
|
||||
import type { BoltProblem, BoltProblemDescription, BoltProblemInput, BoltProblemStatus } from '~/lib/replay/Problems';
|
||||
import { getProblemsUsername, getNutIsAdmin } from '~/lib/replay/Problems';
|
||||
import { getUsername, getNutIsAdmin } from '~/lib/replay/Problems';
|
||||
|
||||
export async function supabaseListAllProblems(): Promise<BoltProblemDescription[]> {
|
||||
try {
|
||||
@ -12,8 +12,6 @@ export async function supabaseListAllProblems(): Promise<BoltProblemDescription[
|
||||
.select('id, created_at, updated_at, title, description, status, keywords, user_id')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
console.log('ListAllProblemsData', data);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
@ -197,7 +195,7 @@ export async function supabaseUpdateProblem(problemId: string, problem: BoltProb
|
||||
const commentInserts = comments.map((comment) => ({
|
||||
problem_id: problemId,
|
||||
content: comment.content,
|
||||
username: comment.username || getProblemsUsername() || 'Anonymous',
|
||||
username: comment.username || getUsername() || 'Anonymous',
|
||||
|
||||
/**
|
||||
* Use timestamp as a unique identifier for the comment.
|
||||
|
@ -10,6 +10,7 @@ import { createHead } from 'remix-island';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { logStore } from './lib/stores/logs';
|
||||
import { initializeAuth, userStore, isLoadingStore } from './lib/stores/auth';
|
||||
import { initializeUserStores } from './lib/stores/user';
|
||||
import { ToastContainer } from 'react-toastify';
|
||||
import { Analytics } from '@vercel/analytics/remix';
|
||||
import { AuthModal } from './components/auth/AuthModal';
|
||||
@ -115,9 +116,14 @@ function AuthProvider({ data }: { data: LoaderData }) {
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.ENV = data.ENV;
|
||||
|
||||
// Initialize auth and user stores
|
||||
initializeAuth().catch((err: Error) => {
|
||||
logStore.logError('Failed to initialize auth', err);
|
||||
});
|
||||
initializeUserStores().catch((err: Error) => {
|
||||
logStore.logError('Failed to initialize user stores', err);
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
|
@ -7,14 +7,14 @@ import { ToastContainerWrapper, Status, Keywords } from './problems';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||
import { useParams } from '@remix-run/react';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import {
|
||||
getProblem,
|
||||
updateProblem as backendUpdateProblem,
|
||||
deleteProblem as backendDeleteProblem,
|
||||
getProblemsUsername,
|
||||
BoltProblemStatus,
|
||||
getNutIsAdmin,
|
||||
} from '~/lib/replay/Problems';
|
||||
import { isAdminStore, usernameStore } from '~/lib/stores/user';
|
||||
import type { BoltProblem, BoltProblemComment } from '~/lib/replay/Problems';
|
||||
|
||||
function Comments({ comments }: { comments: BoltProblemComment[] }) {
|
||||
@ -57,10 +57,12 @@ interface UpdateProblemFormProps {
|
||||
handleSubmit: (content: string) => void;
|
||||
updateText: string;
|
||||
placeholder: string;
|
||||
inputType?: 'textarea' | 'select';
|
||||
options?: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
function UpdateProblemForm(props: UpdateProblemFormProps) {
|
||||
const { handleSubmit, updateText, placeholder } = props;
|
||||
const { handleSubmit, updateText, placeholder, inputType = 'textarea', options = [] } = props;
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const onSubmitClicked = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
@ -74,6 +76,7 @@ function UpdateProblemForm(props: UpdateProblemFormProps) {
|
||||
|
||||
return (
|
||||
<form onSubmit={onSubmitClicked} className="mb-6 p-4 bg-bolt-elements-background-depth-2 rounded-lg">
|
||||
{inputType === 'textarea' ? (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
@ -82,6 +85,23 @@ function UpdateProblemForm(props: UpdateProblemFormProps) {
|
||||
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black placeholder-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-y min-h-[100px]"
|
||||
required
|
||||
/>
|
||||
) : (
|
||||
<select
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
className="w-full p-3 mb-3 bg-bolt-elements-background-depth-3 rounded-md border border-bolt-elements-background-depth-4 text-black focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
required
|
||||
>
|
||||
<option value="" disabled>
|
||||
{placeholder}
|
||||
</option>
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!value.trim()}
|
||||
@ -104,10 +124,12 @@ function UpdateProblemForms({
|
||||
updateProblem: UpdateProblemCallback;
|
||||
deleteProblem: DeleteProblemCallback;
|
||||
}) {
|
||||
const username = useStore(usernameStore);
|
||||
|
||||
const handleAddComment = (content: string) => {
|
||||
const newComment: BoltProblemComment = {
|
||||
timestamp: Date.now(),
|
||||
username: getProblemsUsername(),
|
||||
username,
|
||||
content,
|
||||
};
|
||||
updateProblem((problem) => {
|
||||
@ -158,6 +180,12 @@ function UpdateProblemForms({
|
||||
}));
|
||||
};
|
||||
|
||||
// Convert BoltProblemStatus enum to options array for select
|
||||
const statusOptions = Object.entries(BoltProblemStatus).map(([key, _value]) => ({
|
||||
value: key,
|
||||
label: key,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<UpdateProblemForm handleSubmit={handleAddComment} updateText="Add Comment" placeholder="Add a comment..." />
|
||||
@ -174,7 +202,9 @@ function UpdateProblemForms({
|
||||
<UpdateProblemForm
|
||||
handleSubmit={handleSetStatus}
|
||||
updateText="Set Status"
|
||||
placeholder="Set the status of the problem..."
|
||||
placeholder="Select a status..."
|
||||
inputType="select"
|
||||
options={statusOptions}
|
||||
/>
|
||||
<UpdateProblemForm
|
||||
handleSubmit={handleSetKeywords}
|
||||
@ -199,6 +229,7 @@ const Nothing = () => null;
|
||||
function ViewProblemPage() {
|
||||
const params = useParams();
|
||||
const problemId = params.id;
|
||||
const isAdmin = useStore(isAdminStore);
|
||||
|
||||
if (typeof problemId !== 'string') {
|
||||
throw new Error('Problem ID is required');
|
||||
@ -248,9 +279,7 @@ function ViewProblemPage() {
|
||||
<ProblemViewer problem={problemData} />
|
||||
)}
|
||||
</div>
|
||||
{getNutIsAdmin() && problemData && (
|
||||
<UpdateProblemForms updateProblem={updateProblem} deleteProblem={deleteProblem} />
|
||||
)}
|
||||
{isAdmin && problemData && <UpdateProblemForms updateProblem={updateProblem} deleteProblem={deleteProblem} />}
|
||||
<ToastContainerWrapper />
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
|
@ -10,6 +10,8 @@
|
||||
"deploy": "vercel deploy --prod",
|
||||
"build": "npx remix vite:build",
|
||||
"dev": "node pre-start.cjs && npx remix vite:dev",
|
||||
"dev:supabase": "USE_SUPABASE=true node pre-start.cjs && npx remix vite:dev",
|
||||
"dev:legacy": "USE_SUPABASE=false node pre-start.cjs && npx remix vite:dev",
|
||||
"test": "vitest --run --exclude tests/e2e",
|
||||
"test:watch": "vitest",
|
||||
"test:e2e": "playwright test",
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import { isSupabaseEnabled } from './setup/test-utils';
|
||||
import { isSupabaseEnabled, login, setLoginKey } from './setup/test-utils';
|
||||
|
||||
test('Should be able to load a problem', async ({ page }) => {
|
||||
await page.goto('/problems');
|
||||
@ -34,13 +34,7 @@ test('Should be able to save a problem ', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Log In' }).click();
|
||||
await page.getByRole('textbox', { name: 'Email' }).click();
|
||||
|
||||
const email = process.env.SUPABASE_TEST_USER_EMAIL || '';
|
||||
const password = process.env.SUPABASE_TEST_USER_PASSWORD || '';
|
||||
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(email);
|
||||
await page.getByRole('textbox', { name: 'Email' }).press('Tab');
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill(password);
|
||||
await page.getByRole('textbox', { name: 'Password' }).press('Enter');
|
||||
await login(page);
|
||||
|
||||
await expect(page.getByText('Import the "problem" folder')).toBeVisible({ timeout: 30000 });
|
||||
|
||||
@ -68,3 +62,35 @@ test('Should be able to save a problem ', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Close' }).click();
|
||||
}
|
||||
});
|
||||
|
||||
test('Should be able to update a problem', async ({ page }) => {
|
||||
await page.goto('/problems?showAll=true');
|
||||
await page.getByRole('combobox').selectOption('all');
|
||||
|
||||
await page.getByRole('link', { name: '[test] playwright' }).first().click();
|
||||
expect(await page.getByRole('textbox', { name: 'Set the title of the problem' })).not.toBeVisible();
|
||||
|
||||
if (await isSupabaseEnabled(page)) {
|
||||
await login(page);
|
||||
} else {
|
||||
await setLoginKey(page);
|
||||
}
|
||||
|
||||
const currentTime = new Date();
|
||||
const hours = currentTime.getHours().toString().padStart(2, '0');
|
||||
const minutes = currentTime.getMinutes().toString().padStart(2, '0');
|
||||
const timeString = `${hours}:${minutes}`;
|
||||
const title = `[test] playwright ${timeString}`;
|
||||
|
||||
await page.getByRole('textbox', { name: 'Set the title of the problem' }).click();
|
||||
await page.getByRole('textbox', { name: 'Set the title of the problem' }).fill(title);
|
||||
await page.getByRole('button', { name: 'Set Title' }).click();
|
||||
|
||||
await page.getByRole('heading', { name: title }).click();
|
||||
await page.getByRole('combobox').selectOption('Solved');
|
||||
await page.getByRole('button', { name: 'Set Status' }).click();
|
||||
await page.locator('span').filter({ hasText: 'Solved' }).click();
|
||||
await page.getByRole('combobox').selectOption('Pending');
|
||||
await page.getByRole('button', { name: 'Set Status' }).click();
|
||||
await page.locator('span').filter({ hasText: 'Pending' }).click();
|
||||
});
|
||||
|
@ -53,3 +53,28 @@ export async function clickButton(page: Page, selector: string): Promise<void> {
|
||||
export async function getElementText(page: Page, selector: string): Promise<string> {
|
||||
return page.locator(selector).textContent() as Promise<string>;
|
||||
}
|
||||
|
||||
export async function login(page: Page): Promise<void> {
|
||||
await page.getByRole('button', { name: 'Sign In' }).click();
|
||||
await page.getByRole('textbox', { name: 'Email' }).click();
|
||||
await page.getByRole('textbox', { name: 'Email' }).fill(process.env.SUPABASE_TEST_USER_EMAIL || '');
|
||||
await page.getByRole('textbox', { name: 'Email' }).press('Tab');
|
||||
await page.getByRole('textbox', { name: 'Password' }).fill(process.env.SUPABASE_TEST_USER_PASSWORD || '');
|
||||
await page.getByRole('textbox', { name: 'Password' }).press('Enter');
|
||||
}
|
||||
|
||||
export async function setLoginKey(page: Page): Promise<void> {
|
||||
await page.locator('[data-testid="sidebar-icon"]').click();
|
||||
await page.getByRole('button', { name: 'Settings' }).click();
|
||||
await page.getByRole('button', { name: 'User Info' }).click();
|
||||
await page.getByRole('textbox').nth(1).click();
|
||||
|
||||
await page.getByRole('textbox', { name: 'Enter your username' }).click();
|
||||
await page.getByRole('textbox', { name: 'Enter your login key' }).click();
|
||||
await page.getByRole('textbox', { name: 'Enter your login key' }).fill(process.env.NUT_LOGIN_KEY || '');
|
||||
|
||||
await page.getByRole('textbox', { name: 'Enter your username' }).click();
|
||||
await page.getByRole('textbox', { name: 'Enter your username' }).fill(process.env.NUT_USERNAME || '');
|
||||
|
||||
await page.getByTestId('dialog-close').click();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user