diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..af63504 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +node_modules/ +public/ +tmp/ +log/ +vendor/ +app/views/ \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..cea34cb --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "trailingComma": "all", + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "printWidth": 100 +} \ No newline at end of file diff --git a/app/javascript/entrypoints/application.ts b/app/javascript/entrypoints/application.ts index 9ee6218..5efe348 100644 --- a/app/javascript/entrypoints/application.ts +++ b/app/javascript/entrypoints/application.ts @@ -2,13 +2,12 @@ // The S3 Browser React application uses the s3_browser.html.erb layout, which // loads the s3_browser.tsx entrypoint file. - // To see this message, add the following to the `` section in your // views/layouts/application.html.erb // // <%= vite_client_tag %> // <%= vite_javascript_tag 'application' %> -console.log('Vite ⚡️ Rails') +console.log('Vite ⚡️ Rails'); // If using a TypeScript entrypoint file: // <%= vite_typescript_tag 'application' %> @@ -16,7 +15,7 @@ console.log('Vite ⚡️ Rails') // If you want to use .jsx or .tsx, add the extension: // <%= vite_javascript_tag 'application.jsx' %> -console.log('Visit the guide for more information: ', 'https://vite-ruby.netlify.app/guide/rails') +console.log('Visit the guide for more information: ', 'https://vite-ruby.netlify.app/guide/rails'); // Example: Load Rails libraries in Vite. // diff --git a/app/javascript/entrypoints/s3_browser.scss b/app/javascript/entrypoints/s3_browser.scss index 1ac573f..abbd7f1 100644 --- a/app/javascript/entrypoints/s3_browser.scss +++ b/app/javascript/entrypoints/s3_browser.scss @@ -9,4 +9,4 @@ // --bs-accordion-border-radius: Changes the border radius. // --bs-accordion-btn-focus-box-shadow: Removes the blue outline when an item is focused. */ -@import "bootstrap"; \ No newline at end of file +@import 'bootstrap'; diff --git a/app/javascript/entrypoints/s3_browser.tsx b/app/javascript/entrypoints/s3_browser.tsx index 2eea355..548d47f 100644 --- a/app/javascript/entrypoints/s3_browser.tsx +++ b/app/javascript/entrypoints/s3_browser.tsx @@ -2,9 +2,9 @@ import { createRoot } from 'react-dom/client'; import App from '../../s3browser/src/app/App'; const s3BrowserAppElement = document.getElementById('s3-browser-app'); -if (!s3BrowserAppElement ) throw new Error('S3 Browser app root element not found'); +if (!s3BrowserAppElement) throw new Error('S3 Browser app root element not found'); const s3BrowserRoot = createRoot(s3BrowserAppElement); -s3BrowserRoot .render() +s3BrowserRoot.render(); -console.log('S3 Browser React app load complete!'); \ No newline at end of file +console.log('S3 Browser React app load complete!'); diff --git a/app/s3browser/src/app/App.tsx b/app/s3browser/src/app/App.tsx index 36aa83f..e1c9904 100644 --- a/app/s3browser/src/app/App.tsx +++ b/app/s3browser/src/app/App.tsx @@ -9,7 +9,7 @@ const App = () => { - ) -} + ); +}; -export default App; \ No newline at end of file +export default App; diff --git a/app/s3browser/src/app/provider.tsx b/app/s3browser/src/app/provider.tsx index b9e62f6..9a75178 100644 --- a/app/s3browser/src/app/provider.tsx +++ b/app/s3browser/src/app/provider.tsx @@ -56,4 +56,4 @@ export const AppProvider: FC> = ({ children }) => { ); -} \ No newline at end of file +}; diff --git a/app/s3browser/src/app/router.tsx b/app/s3browser/src/app/router.tsx index 399556f..95d2638 100644 --- a/app/s3browser/src/app/router.tsx +++ b/app/s3browser/src/app/router.tsx @@ -58,7 +58,9 @@ export const createAppRouter = (queryClient: QueryClient) => path: ':bucketName/object-details', lazy: () => import('./routes/object-details').then(convert(queryClient)), // This route uses useSuspenseQuery, so we want to ensure any errors are caught by the route error boundary - errorElement: , + errorElement: ( + + ), }, ], }, @@ -78,4 +80,4 @@ export const AppRouter = () => { const router = useMemo(() => createAppRouter(queryClient), [queryClient]); return ; -}; \ No newline at end of file +}; diff --git a/app/s3browser/src/app/routes/bucket-contents.tsx b/app/s3browser/src/app/routes/bucket-contents.tsx index f6dd263..2ea2ca2 100644 --- a/app/s3browser/src/app/routes/bucket-contents.tsx +++ b/app/s3browser/src/app/routes/bucket-contents.tsx @@ -4,16 +4,18 @@ import { getBucketContentsQueryOptions } from '@/features/file-browser/api/get-b import BucketContentsTable from '@/features/file-browser/components/bucket-contents-table'; import { normalizePrefix } from '@/features/file-browser/utils/format-utils'; -export const clientLoader = (queryClient: QueryClient) => async ({ params, request }: LoaderFunctionArgs) => { - const bucketName = params.bucketName as string; - const url = new URL(request.url); - const prefix = normalizePrefix(url.searchParams.get('prefix') ?? ''); - const query = getBucketContentsQueryOptions(bucketName, prefix); +export const clientLoader = + (queryClient: QueryClient) => + async ({ params, request }: LoaderFunctionArgs) => { + const bucketName = params.bucketName as string; + const url = new URL(request.url); + const prefix = normalizePrefix(url.searchParams.get('prefix') ?? ''); + const query = getBucketContentsQueryOptions(bucketName, prefix); - // Our API returns results in ~1-2 seconds for large buckets, so we don't want to - // await this and block the UI. Instead, we let the component handle the loading state. - queryClient.prefetchQuery(query); -}; + // Our API returns results in ~1-2 seconds for large buckets, so we don't want to + // await this and block the UI. Instead, we let the component handle the loading state. + queryClient.prefetchQuery(query); + }; const BucketContentsRoute = () => { const params = useParams(); @@ -27,4 +29,4 @@ const BucketContentsRoute = () => { ); }; -export default BucketContentsRoute; \ No newline at end of file +export default BucketContentsRoute; diff --git a/app/s3browser/src/app/routes/buckets.tsx b/app/s3browser/src/app/routes/buckets.tsx index ef4f0e1..c58d812 100644 --- a/app/s3browser/src/app/routes/buckets.tsx +++ b/app/s3browser/src/app/routes/buckets.tsx @@ -17,4 +17,4 @@ const BucketsRoute = () => { ); }; -export default BucketsRoute; \ No newline at end of file +export default BucketsRoute; diff --git a/app/s3browser/src/app/routes/not-found.tsx b/app/s3browser/src/app/routes/not-found.tsx index 8ed5656..8e1e14a 100644 --- a/app/s3browser/src/app/routes/not-found.tsx +++ b/app/s3browser/src/app/routes/not-found.tsx @@ -10,4 +10,4 @@ const NotFoundRoute = () => { ); }; -export default NotFoundRoute; \ No newline at end of file +export default NotFoundRoute; diff --git a/app/s3browser/src/app/routes/object-details.tsx b/app/s3browser/src/app/routes/object-details.tsx index 553ad7d..f47b655 100644 --- a/app/s3browser/src/app/routes/object-details.tsx +++ b/app/s3browser/src/app/routes/object-details.tsx @@ -1,16 +1,21 @@ import { LoaderFunctionArgs, useParams, useSearchParams } from 'react-router'; import { QueryClient } from '@tanstack/react-query'; -import { getObjectDetailsQueryOptions, useObjectDetailsSuspenseQuery } from '@/features/file-browser/api/get-object-details'; +import { + getObjectDetailsQueryOptions, + useObjectDetailsSuspenseQuery, +} from '@/features/file-browser/api/get-object-details'; import ObjectDetailDisplay from '@/features/file-browser/components/object-detail-display'; -export const clientLoader = (queryClient: QueryClient) => async ({ params, request }: LoaderFunctionArgs) => { - const bucketName = params.bucketName as string; - const url = new URL(request.url); - const key = url.searchParams.get('prefix') ?? ''; - const query = getObjectDetailsQueryOptions(bucketName, key); +export const clientLoader = + (queryClient: QueryClient) => + async ({ params, request }: LoaderFunctionArgs) => { + const bucketName = params.bucketName as string; + const url = new URL(request.url); + const key = url.searchParams.get('prefix') ?? ''; + const query = getObjectDetailsQueryOptions(bucketName, key); - await queryClient.prefetchQuery(query); -}; + await queryClient.prefetchQuery(query); + }; const ObjectDetailsRoute = () => { const params = useParams(); @@ -27,4 +32,4 @@ const ObjectDetailsRoute = () => { ); }; -export default ObjectDetailsRoute; \ No newline at end of file +export default ObjectDetailsRoute; diff --git a/app/s3browser/src/components/errors/main.tsx b/app/s3browser/src/components/errors/main.tsx index 71ba1a4..146ad6d 100644 --- a/app/s3browser/src/components/errors/main.tsx +++ b/app/s3browser/src/components/errors/main.tsx @@ -4,4 +4,4 @@ export const MainErrorFallback = () => {

Ooops, something went wrong

); -}; \ No newline at end of file +}; diff --git a/app/s3browser/src/components/errors/route-error.tsx b/app/s3browser/src/components/errors/route-error.tsx index cdaf83c..29c48b2 100644 --- a/app/s3browser/src/components/errors/route-error.tsx +++ b/app/s3browser/src/components/errors/route-error.tsx @@ -1,5 +1,5 @@ -import { Container } from "react-bootstrap"; -import { useRouteError, isRouteErrorResponse } from "react-router"; +import { Container } from 'react-bootstrap'; +import { useRouteError, isRouteErrorResponse } from 'react-router'; type RouteErrorFallbackProps = { errorMessage?: string; @@ -13,12 +13,14 @@ export const RouteErrorFallback: React.FC = ({ errorMes

Oops, something went wrong


{errorMessage || 'The application encountered an error.'}

- {isRouteErrorResponse(error) && ( - -

{error.status} {error.statusText}

-
{JSON.stringify(error.data, null, 2)}
-
- )} + {isRouteErrorResponse(error) && ( + +

+ {error.status} {error.statusText} +

+
{JSON.stringify(error.data, null, 2)}
+
+ )} ); -}; \ No newline at end of file +}; diff --git a/app/s3browser/src/components/layouts/main-layout.tsx b/app/s3browser/src/components/layouts/main-layout.tsx index bff672c..29c4997 100644 --- a/app/s3browser/src/components/layouts/main-layout.tsx +++ b/app/s3browser/src/components/layouts/main-layout.tsx @@ -16,4 +16,4 @@ const MainLayout = () => { ); }; -export default MainLayout; \ No newline at end of file +export default MainLayout; diff --git a/app/s3browser/src/components/ui/notifications/notification.tsx b/app/s3browser/src/components/ui/notifications/notification.tsx index a2e7069..97d76cb 100644 --- a/app/s3browser/src/components/ui/notifications/notification.tsx +++ b/app/s3browser/src/components/ui/notifications/notification.tsx @@ -1,22 +1,16 @@ -import { Toast } from "react-bootstrap"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { - faCircleCheck, - faCircleXmark, -} from "@fortawesome/free-solid-svg-icons"; -import { - faTriangleExclamation, - faCircleInfo -} from "@fortawesome/free-solid-svg-icons"; -import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; +import { Toast } from 'react-bootstrap'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faCircleCheck, faCircleXmark } from '@fortawesome/free-solid-svg-icons'; +import { faTriangleExclamation, faCircleInfo } from '@fortawesome/free-solid-svg-icons'; +import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'; -type NotificationType = "success" | "error" | "warning" | "info"; +type NotificationType = 'success' | 'error' | 'warning' | 'info'; const VARIANT_MAP: Record = { - success: "success", - error: "danger", - warning: "warning", - info: "info", + success: 'success', + error: 'danger', + warning: 'warning', + info: 'info', }; const ICON_MAP: Record = { @@ -50,18 +44,11 @@ export const Notification = ({ autohide delay={5000} > - - + + {title} - {message && ( - {message} - )} + {message && {message}} ); }; diff --git a/app/s3browser/src/components/ui/notifications/notifications.tsx b/app/s3browser/src/components/ui/notifications/notifications.tsx index 3313429..3eee73e 100644 --- a/app/s3browser/src/components/ui/notifications/notifications.tsx +++ b/app/s3browser/src/components/ui/notifications/notifications.tsx @@ -1,6 +1,6 @@ -import { ToastContainer } from "react-bootstrap"; -import { useNotifications } from "@/stores/notifications-store"; -import { Notification } from "./notification"; +import { ToastContainer } from 'react-bootstrap'; +import { useNotifications } from '@/stores/notifications-store'; +import { Notification } from './notification'; export const Notifications = () => { const { notifications, dismissNotification } = useNotifications(); @@ -16,4 +16,4 @@ export const Notifications = () => { ))} ); -} \ No newline at end of file +}; diff --git a/app/s3browser/src/components/ui/table-builder/table-body.tsx b/app/s3browser/src/components/ui/table-builder/table-body.tsx index 6112867..c3539cb 100644 --- a/app/s3browser/src/components/ui/table-builder/table-body.tsx +++ b/app/s3browser/src/components/ui/table-builder/table-body.tsx @@ -1,18 +1,14 @@ -import { ColumnDef, useReactTable } from '@tanstack/react-table' +import { ColumnDef, useReactTable } from '@tanstack/react-table'; import Spinner from 'react-bootstrap/Spinner'; -import TableRow from './table-row' +import TableRow from './table-row'; type TableBodyProps = { table: ReturnType>; columns: ColumnDef[]; isLoading?: boolean; -} +}; -const TableBody = ({ - table, - columns, - isLoading, -}: TableBodyProps) => { +const TableBody = ({ table, columns, isLoading }: TableBodyProps) => { if (isLoading) { return ( @@ -50,4 +46,4 @@ const TableBody = ({ ); }; -export default TableBody; \ No newline at end of file +export default TableBody; diff --git a/app/s3browser/src/components/ui/table-builder/table-builder.tsx b/app/s3browser/src/components/ui/table-builder/table-builder.tsx index 1c65c62..b0ef926 100644 --- a/app/s3browser/src/components/ui/table-builder/table-builder.tsx +++ b/app/s3browser/src/components/ui/table-builder/table-builder.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useState } from 'react'; import { getCoreRowModel, @@ -9,17 +9,17 @@ import { getPaginationRowModel, PaginationState, Updater, -} from '@tanstack/react-table' -import { Table as BTable } from 'react-bootstrap' -import TableHeader from './table-header' -import TablePagination from './table-pagination' -import TableBody from './table-body' +} from '@tanstack/react-table'; +import { Table as BTable } from 'react-bootstrap'; +import TableHeader from './table-header'; +import TablePagination from './table-pagination'; +import TableBody from './table-body'; interface TableBuilderProps { - data: T[] - columns: ColumnDef[] - initialSorting?: SortingState, - pageSize?: number, + data: T[]; + columns: ColumnDef[]; + initialSorting?: SortingState; + pageSize?: number; pagination?: PaginationState; onPaginationChange?: (updater: Updater) => void; isLoading?: boolean; @@ -37,22 +37,22 @@ function TableBuilder({ pageSize = DEFAULT_PAGE_SIZE, pagination, onPaginationChange, - isLoading + isLoading, }: TableBuilderProps) { // You can disable sorting specific columns or specify custom sorting functions in the column definitions // Docs: https://tanstack.com/table/latest/docs/api/features/sorting#column-def-options - const [sorting, setSorting] = useState(initialSorting) + const [sorting, setSorting] = useState(initialSorting); const [internalPagination, setInternalPagination] = useState({ pageIndex: 0, pageSize, - }) + }); // Determine if pagination is controlled externally (via props) or internally (via component state) const isPaginationControlled = pagination !== undefined && onPaginationChange !== undefined; const effectivePagination = isPaginationControlled ? pagination : internalPagination; const effectiveOnPaginationChange = isPaginationControlled ? onPaginationChange - : setInternalPagination + : setInternalPagination; const table = useReactTable({ data, @@ -60,13 +60,13 @@ function TableBuilder({ getCoreRowModel: getCoreRowModel(), state: { sorting, - pagination: effectivePagination + pagination: effectivePagination, }, getSortedRowModel: getSortedRowModel(), onSortingChange: setSorting, onPaginationChange: effectiveOnPaginationChange, getPaginationRowModel: getPaginationRowModel(), - }) + }); return ( <> @@ -76,18 +76,12 @@ function TableBuilder({ {table.getHeaderGroups().map((headerGroup) => ( - + ))} - + - ) + ); } -export default TableBuilder; \ No newline at end of file +export default TableBuilder; diff --git a/app/s3browser/src/components/ui/table-builder/table-header.tsx b/app/s3browser/src/components/ui/table-builder/table-header.tsx index c5413ff..52b63cd 100644 --- a/app/s3browser/src/components/ui/table-builder/table-header.tsx +++ b/app/s3browser/src/components/ui/table-builder/table-header.tsx @@ -1,29 +1,29 @@ -import { flexRender, Header, HeaderGroup } from '@tanstack/react-table' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons' +import { flexRender, Header, HeaderGroup } from '@tanstack/react-table'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSort, faSortUp, faSortDown } from '@fortawesome/free-solid-svg-icons'; interface TableHeaderProps { - headerGroup: HeaderGroup + headerGroup: HeaderGroup; } function TableHeader({ headerGroup }: TableHeaderProps) { const renderSortingIcon = (sortDirection: 'asc' | 'desc' | null) => { - const sharedClassNames = 'ms-2 flex-shrink-0 mt-1' + const sharedClassNames = 'ms-2 flex-shrink-0 mt-1'; if (sortDirection === 'asc') { - return + return ; } if (sortDirection === 'desc') { - return + return ; } - return - } + return ; + }; const createColumnHeader = (header: Header) => { - if (header.isPlaceholder) return null + if (header.isPlaceholder) return null; - const sharedClassNames = 'fw-semibold d-inline-flex m-0 p-0 align-items-start text-start' - const headerText = flexRender(header.column.columnDef.header, header.getContext()) + const sharedClassNames = 'fw-semibold d-inline-flex m-0 p-0 align-items-start text-start'; + const headerText = flexRender(header.column.columnDef.header, header.getContext()); if (header.column.getCanSort()) { return ( @@ -35,23 +35,26 @@ function TableHeader({ headerGroup }: TableHeaderProps) { {headerText} {renderSortingIcon(header.column.getIsSorted() || null)} - ) + ); } - return
{headerText}
- } + return
{headerText}
; + }; return ( {headerGroup.headers.map((header) => ( - + {createColumnHeader(header)} ))} - ) + ); } -export default TableHeader; \ No newline at end of file +export default TableHeader; diff --git a/app/s3browser/src/components/ui/table-builder/table-pagination.tsx b/app/s3browser/src/components/ui/table-builder/table-pagination.tsx index 13cd557..9a91f41 100644 --- a/app/s3browser/src/components/ui/table-builder/table-pagination.tsx +++ b/app/s3browser/src/components/ui/table-builder/table-pagination.tsx @@ -1,24 +1,24 @@ -import { Table } from '@tanstack/react-table' -import { Pagination } from 'react-bootstrap' +import { Table } from '@tanstack/react-table'; +import { Pagination } from 'react-bootstrap'; interface TablePaginationProps { - table: Table + table: Table; } // Based on https://tanstack.com/table/v8/docs/framework/react/examples/pagination function TablePagination({ table }: TablePaginationProps) { - const { pageIndex } = table.getState().pagination - const pageCount = table.getPageCount() - const startRow = pageIndex * table.getState().pagination.pageSize + 1 - const endRow = Math.min((pageIndex + 1) * table.getState().pagination.pageSize, table.getFilteredRowModel().rows.length) + const { pageIndex } = table.getState().pagination; + const pageCount = table.getPageCount(); + const startRow = pageIndex * table.getState().pagination.pageSize + 1; + const endRow = Math.min( + (pageIndex + 1) * table.getState().pagination.pageSize, + table.getFilteredRowModel().rows.length, + ); return ( {/* TODO: Display how many items are being shown */} - table.firstPage()} - disabled={!table.getCanPreviousPage()} - /> + table.firstPage()} disabled={!table.getCanPreviousPage()} /> table.previousPage()} disabled={!table.getCanPreviousPage()} @@ -26,19 +26,13 @@ function TablePagination({ table }: TablePaginationProps) { {pageIndex + 1} of {pageCount} - table.nextPage()} - disabled={!table.getCanNextPage()} - /> - table.lastPage()} - disabled={!table.getCanNextPage()} - /> + table.nextPage()} disabled={!table.getCanNextPage()} /> + table.lastPage()} disabled={!table.getCanNextPage()} />
Showing {startRow}-{endRow} of {table.getFilteredRowModel().rows.length}
- ) + ); } -export default TablePagination \ No newline at end of file +export default TablePagination; diff --git a/app/s3browser/src/components/ui/table-builder/table-row.tsx b/app/s3browser/src/components/ui/table-builder/table-row.tsx index 6572532..9691637 100644 --- a/app/s3browser/src/components/ui/table-builder/table-row.tsx +++ b/app/s3browser/src/components/ui/table-builder/table-row.tsx @@ -1,19 +1,17 @@ -import { flexRender, Row, Cell } from '@tanstack/react-table' +import { flexRender, Row, Cell } from '@tanstack/react-table'; interface TableRowProps { - row: Row + row: Row; } const TableRow = ({ row }: TableRowProps) => { return ( {row.getVisibleCells().map((cell: Cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} - ) -} + ); +}; -export default TableRow; \ No newline at end of file +export default TableRow; diff --git a/app/s3browser/src/features/file-browser/api/get-bucket-contents.ts b/app/s3browser/src/features/file-browser/api/get-bucket-contents.ts index 37db474..cb983a1 100644 --- a/app/s3browser/src/features/file-browser/api/get-bucket-contents.ts +++ b/app/s3browser/src/features/file-browser/api/get-bucket-contents.ts @@ -3,10 +3,7 @@ import { BucketContentsResponse } from '@/types/api'; import { api } from '@/lib/api-client'; import { QueryConfig } from '@/lib/react-query'; -const getBucketContents = ( - bucket: string, - prefix: string, -): Promise => { +const getBucketContents = (bucket: string, prefix: string): Promise => { const params = new URLSearchParams(); if (prefix) { params.set('prefix', prefix); @@ -31,7 +28,11 @@ export const getBucketContentsQueryOptions = (bucket: string, prefix: string) => }); }; -export const useBucketContentsQuery = ({ bucket, prefix, queryConfig }: UseBucketContentsOptions) => { +export const useBucketContentsQuery = ({ + bucket, + prefix, + queryConfig, +}: UseBucketContentsOptions) => { return useQuery({ ...getBucketContentsQueryOptions(bucket, prefix), ...queryConfig, diff --git a/app/s3browser/src/features/file-browser/api/get-object-details.ts b/app/s3browser/src/features/file-browser/api/get-object-details.ts index 2a84803..03fa2aa 100644 --- a/app/s3browser/src/features/file-browser/api/get-object-details.ts +++ b/app/s3browser/src/features/file-browser/api/get-object-details.ts @@ -3,10 +3,7 @@ import { ObjectDetails } from '@/types/api'; import { api } from '@/lib/api-client'; import { QueryConfig } from '@/lib/react-query'; -const getObjectDetails = ( - bucket: string, - key: string, -): Promise => { +const getObjectDetails = (bucket: string, key: string): Promise => { const params = new URLSearchParams(); if (key) { params.set('key', key); @@ -31,8 +28,11 @@ export const getObjectDetailsQueryOptions = (bucket: string, key: string) => { }); }; - -export const useObjectDetailsSuspenseQuery = ({ bucket, key, queryConfig }: UseObjectDetailsOptions) => { +export const useObjectDetailsSuspenseQuery = ({ + bucket, + key, + queryConfig, +}: UseObjectDetailsOptions) => { return useSuspenseQuery({ ...getObjectDetailsQueryOptions(bucket, key), ...queryConfig, diff --git a/app/s3browser/src/features/file-browser/components/breadcrumbs.tsx b/app/s3browser/src/features/file-browser/components/breadcrumbs.tsx index 8c86252..4c1108f 100644 --- a/app/s3browser/src/features/file-browser/components/breadcrumbs.tsx +++ b/app/s3browser/src/features/file-browser/components/breadcrumbs.tsx @@ -6,14 +6,9 @@ interface BreadcrumbsProps { prefix?: string; } -const buildSegments = ( - bucketName?: string, - prefix?: string, -) => { +const buildSegments = (bucketName?: string, prefix?: string) => { const allBucketsPath = '/browse/buckets'; - const segments = [ - { label: 'All Buckets', to: allBucketsPath }, - ]; + const segments = [{ label: 'All Buckets', to: allBucketsPath }]; if (!bucketName) return segments; @@ -61,4 +56,4 @@ const Breadcrumbs = ({ bucketName, prefix }: BreadcrumbsProps) => { ); }; -export default Breadcrumbs; \ No newline at end of file +export default Breadcrumbs; diff --git a/app/s3browser/src/features/file-browser/components/bucket-contents-table.tsx b/app/s3browser/src/features/file-browser/components/bucket-contents-table.tsx index 18913f5..ddd2045 100644 --- a/app/s3browser/src/features/file-browser/components/bucket-contents-table.tsx +++ b/app/s3browser/src/features/file-browser/components/bucket-contents-table.tsx @@ -4,7 +4,7 @@ import { columnDefs } from '../utils/bucket-contents-column-defs'; import { toBucketItems } from '../utils/transform-to-bucket-items'; import { useBucketContentsQuery } from '../api/get-bucket-contents'; import TableBuilder from '@/components/ui/table-builder/table-builder'; -import { usePagination } from '../hooks/use-pagination' +import { usePagination } from '../hooks/use-pagination'; import { normalizePrefix } from '../utils/format-utils'; const BucketContentsTable = () => { @@ -32,7 +32,9 @@ const BucketContentsTable = () => { return (
-

{currentDirectory}/

+

+ {currentDirectory}/ +

{ ); }; -export default BucketContentsTable; \ No newline at end of file +export default BucketContentsTable; diff --git a/app/s3browser/src/features/file-browser/components/bucket-list.tsx b/app/s3browser/src/features/file-browser/components/bucket-list.tsx index 6b5ef29..82127da 100644 --- a/app/s3browser/src/features/file-browser/components/bucket-list.tsx +++ b/app/s3browser/src/features/file-browser/components/bucket-list.tsx @@ -24,4 +24,4 @@ const BucketList = () => { ); }; -export default BucketList; \ No newline at end of file +export default BucketList; diff --git a/app/s3browser/src/features/file-browser/components/object-detail-display.tsx b/app/s3browser/src/features/file-browser/components/object-detail-display.tsx index b846a88..8aa7145 100644 --- a/app/s3browser/src/features/file-browser/components/object-detail-display.tsx +++ b/app/s3browser/src/features/file-browser/components/object-detail-display.tsx @@ -1,6 +1,12 @@ import { useMemo } from 'react'; import { ObjectDetails } from '@/types/api'; -import { formatSize, formatLastModified, capitalizeStr, extractName, extractFileExtension } from '../utils/format-utils'; +import { + formatSize, + formatLastModified, + capitalizeStr, + extractName, + extractFileExtension, +} from '../utils/format-utils'; import ObjectDetailField from './object-detail-field'; const displayRetrievalTime = (archiveStatus: string | null) => { @@ -15,22 +21,16 @@ const displayRetrievalTime = (archiveStatus: string | null) => { }; const displayAccessTierLabel = (archiveStatus: string | null) => - archiveStatus ? capitalizeStr(archiveStatus) : 'Frequent Access, Infrequent Access, or Archive Instant Access tier'; - + archiveStatus + ? capitalizeStr(archiveStatus) + : 'Frequent Access, Infrequent Access, or Archive Instant Access tier'; type ObjectDetailDisplayProps = { objectDetails: ObjectDetails; }; const ObjectDetailDisplay = ({ objectDetails }: ObjectDetailDisplayProps) => { - const { - key, - size, - lastModified, - storageClass, - archiveStatus, - restoreStatus, - } = objectDetails; + const { key, size, lastModified, storageClass, archiveStatus, restoreStatus } = objectDetails; const isNonStandard = storageClass !== 'STANDARD'; const fileName = useMemo(() => extractName(key), [key]); @@ -43,10 +43,7 @@ const ObjectDetailDisplay = ({ objectDetails }: ObjectDetailDisplayProps) => {
Object Overview
- + @@ -57,10 +54,7 @@ const ObjectDetailDisplay = ({ objectDetails }: ObjectDetailDisplayProps) => {
Storage Details
- + {isNonStandard && ( <> @@ -75,7 +69,11 @@ const ObjectDetailDisplay = ({ objectDetails }: ObjectDetailDisplayProps) => { /> )} @@ -85,4 +83,4 @@ const ObjectDetailDisplay = ({ objectDetails }: ObjectDetailDisplayProps) => { ); }; -export default ObjectDetailDisplay; \ No newline at end of file +export default ObjectDetailDisplay; diff --git a/app/s3browser/src/features/file-browser/components/object-detail-field.tsx b/app/s3browser/src/features/file-browser/components/object-detail-field.tsx index e2f4e92..86cf0a8 100644 --- a/app/s3browser/src/features/file-browser/components/object-detail-field.tsx +++ b/app/s3browser/src/features/file-browser/components/object-detail-field.tsx @@ -8,9 +8,7 @@ type ObjectDetailFieldProps = { const ObjectDetailField = ({ label, value, hint }: ObjectDetailFieldProps) => (
-
- {label} -
+
{label}
{value} {hint && {hint}} @@ -18,4 +16,4 @@ const ObjectDetailField = ({ label, value, hint }: ObjectDetailFieldProps) => (
); -export default ObjectDetailField; \ No newline at end of file +export default ObjectDetailField; diff --git a/app/s3browser/src/features/file-browser/hooks/use-pagination.tsx b/app/s3browser/src/features/file-browser/hooks/use-pagination.tsx index bdf5856..5cc3e34 100644 --- a/app/s3browser/src/features/file-browser/hooks/use-pagination.tsx +++ b/app/s3browser/src/features/file-browser/hooks/use-pagination.tsx @@ -1,12 +1,12 @@ -import { useSearchParams } from "react-router"; -import { PaginationState, Updater } from "@tanstack/react-table"; +import { useSearchParams } from 'react-router'; +import { PaginationState, Updater } from '@tanstack/react-table'; const PAGE_SIZE = 50; export const usePagination = () => { const [searchParams, setSearchParams] = useSearchParams(); - const pageFromUrl = parseInt(searchParams.get("page") ?? "1", 10); + const pageFromUrl = parseInt(searchParams.get('page') ?? '1', 10); const pageIndex = pageFromUrl > 0 ? pageFromUrl - 1 : 0; const pagination: PaginationState = { @@ -17,25 +17,27 @@ export const usePagination = () => { // TanStack Table calls onPaginationChange with either a new PaginationState // or an updater function (prevState) => newState. const onPaginationChange = (updater: Updater) => { - const newState = - typeof updater === "function" ? updater(pagination) : updater; + const newState = typeof updater === 'function' ? updater(pagination) : updater; // Don't set the URL if the page hasn't actually changed. // This prevents TanStack Table's autoResetPageIndex from // pushing empty history entries on every data load. if (newState.pageIndex === pageIndex) return; - setSearchParams((prev) => { - const next = new URLSearchParams(prev); - // Store 1-based page in URL; don't include the param on page 1 for a cleaner URL - if (newState.pageIndex <= 0) { - next.delete("page"); - } else { - next.set("page", (newState.pageIndex + 1).toString()); - } - return next; - }, { replace: true }); + setSearchParams( + (prev) => { + const next = new URLSearchParams(prev); + // Store 1-based page in URL; don't include the param on page 1 for a cleaner URL + if (newState.pageIndex <= 0) { + next.delete('page'); + } else { + next.set('page', (newState.pageIndex + 1).toString()); + } + return next; + }, + { replace: true }, + ); }; return { pagination, onPaginationChange }; -}; \ No newline at end of file +}; diff --git a/app/s3browser/src/features/file-browser/utils/bucket-contents-column-defs.tsx b/app/s3browser/src/features/file-browser/utils/bucket-contents-column-defs.tsx index 2f69082..048e0c3 100644 --- a/app/s3browser/src/features/file-browser/utils/bucket-contents-column-defs.tsx +++ b/app/s3browser/src/features/file-browser/utils/bucket-contents-column-defs.tsx @@ -1,9 +1,14 @@ -import { Link } from 'react-router' -import { createColumnHelper } from '@tanstack/react-table' -import { BucketItem } from '@/types/api' -import { capitalizeStr, extractFileExtension, formatSize, formatLastModified } from './format-utils'; +import { Link } from 'react-router'; +import { createColumnHelper } from '@tanstack/react-table'; +import { BucketItem } from '@/types/api'; +import { + capitalizeStr, + extractFileExtension, + formatSize, + formatLastModified, +} from './format-utils'; -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper(); const sortByTypeAndExtension = (a: BucketItem, b: BucketItem) => { if (a.type !== b.type) { @@ -21,15 +26,20 @@ const sortByTypeAndExtension = (a: BucketItem, b: BucketItem) => { } return a.name.localeCompare(b.name); -} +}; export const columnDefs = (bucket: string) => [ columnHelper.accessor('name', { header: 'Name', cell: ({ row }) => { const bucketPath = `/browse/buckets/${bucket}`; - const prefix = row.original.fullPath ? `?prefix=${encodeURIComponent(row.original.fullPath)}` : ''; - const url = row.original.type === 'folder' ? `${bucketPath}${prefix}` : `${bucketPath}/object-details${prefix}`; + const prefix = row.original.fullPath + ? `?prefix=${encodeURIComponent(row.original.fullPath)}` + : ''; + const url = + row.original.type === 'folder' + ? `${bucketPath}${prefix}` + : `${bucketPath}/object-details${prefix}`; return ( [ }, sortingFn: 'datetime', // This is the slowest part of our sorting sortDescFirst: false, - sortUndefined: 'last' + sortUndefined: 'last', }), columnHelper.accessor('type', { header: 'Type', - cell: ({ row }) => row.original.type === 'folder' ? 'Folder' : extractFileExtension(row.original.name), + cell: ({ row }) => + row.original.type === 'folder' ? 'Folder' : extractFileExtension(row.original.name), // Sorts folders first, then sorts objects by file extension sortingFn: (rowA, rowB) => sortByTypeAndExtension(rowA.original, rowB.original), }), columnHelper.accessor('storageClass', { header: 'Storage Class', - cell: ({ row }) => row.original.type === 'object' ? capitalizeStr(row.original.storageClass) : '-', + cell: ({ row }) => + row.original.type === 'object' ? capitalizeStr(row.original.storageClass) : '-', sortDescFirst: false, - sortUndefined: 'last' + sortUndefined: 'last', }), columnHelper.accessor('size', { header: 'Size', @@ -72,6 +84,6 @@ export const columnDefs = (bucket: string) => [ return row.type === 'object' ? formatSize(info.getValue() as number) : '-'; }, sortDescFirst: false, - sortUndefined: 'last' + sortUndefined: 'last', }), -] +]; diff --git a/app/s3browser/src/features/file-browser/utils/bucket-list-column-defs.tsx b/app/s3browser/src/features/file-browser/utils/bucket-list-column-defs.tsx index e7146e3..f2d4779 100644 --- a/app/s3browser/src/features/file-browser/utils/bucket-list-column-defs.tsx +++ b/app/s3browser/src/features/file-browser/utils/bucket-list-column-defs.tsx @@ -1,8 +1,8 @@ -import { Link } from 'react-router' -import { createColumnHelper } from '@tanstack/react-table' -import { Bucket } from '@/types/api' +import { Link } from 'react-router'; +import { createColumnHelper } from '@tanstack/react-table'; +import { Bucket } from '@/types/api'; -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper(); export const columnDefs = [ columnHelper.accessor('name', { @@ -14,11 +14,11 @@ export const columnDefs = [ > {row.original.name} - ) + ), }), columnHelper.accessor('description', { header: 'Description', cell: (info) => info.getValue(), - enableSorting: false + enableSorting: false, }), -] +]; diff --git a/app/s3browser/src/features/file-browser/utils/format-utils.ts b/app/s3browser/src/features/file-browser/utils/format-utils.ts index fa0449a..dc68cb9 100644 --- a/app/s3browser/src/features/file-browser/utils/format-utils.ts +++ b/app/s3browser/src/features/file-browser/utils/format-utils.ts @@ -15,14 +15,15 @@ const formatSize = (sizeInBytes: number) => { } return unitIndex === 0 ? `${size} ${units[unitIndex]}` : `${size.toFixed(2)} ${units[unitIndex]}`; -} +}; const capitalizeStr = (str: string) => { - return str.toLowerCase() + return str + .toLowerCase() .split('_') - .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); -} +}; const extractName = (fullPath: string): string => { const trimmed = fullPath.endsWith('/') ? fullPath.slice(0, -1) : fullPath; @@ -34,13 +35,22 @@ const formatLastModified = (dateString: string): string => { const date = new Date(dateString); const pad = (n: number) => n.toString().padStart(2, '0'); - return `${date.toLocaleString('en-US', { month: 'long' })} ${date.getDate()}, ${date.getFullYear()}, ` + - `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; + return ( + `${date.toLocaleString('en-US', { month: 'long' })} ${date.getDate()}, ${date.getFullYear()}, ` + + `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` + ); }; const extractFileExtension = (fileName: string) => { const parts = fileName.split('.'); return parts.length > 1 ? parts.pop() : 'unknown'; -} +}; -export { formatSize, capitalizeStr, extractName, formatLastModified, extractFileExtension, normalizePrefix }; \ No newline at end of file +export { + formatSize, + capitalizeStr, + extractName, + formatLastModified, + extractFileExtension, + normalizePrefix, +}; diff --git a/app/s3browser/src/features/file-browser/utils/transform-to-bucket-items.ts b/app/s3browser/src/features/file-browser/utils/transform-to-bucket-items.ts index b906f11..6369d88 100644 --- a/app/s3browser/src/features/file-browser/utils/transform-to-bucket-items.ts +++ b/app/s3browser/src/features/file-browser/utils/transform-to-bucket-items.ts @@ -1,4 +1,3 @@ - import { BucketContentsResponse, BucketItem } from '@/types/api'; import { extractName } from './format-utils'; @@ -10,7 +9,7 @@ export const toBucketItems = (response: BucketContentsResponse): BucketItem[] => name: extractName(prefix), fullPath: prefix, })); - + const objectItems: BucketItem[] = response.objects.map((obj) => ({ type: 'object', name: extractName(obj.key), @@ -19,6 +18,6 @@ export const toBucketItems = (response: BucketContentsResponse): BucketItem[] => storageClass: obj.storageClass, lastModified: obj.lastModified, })); - + return [...folderItems, ...objectItems]; -}; \ No newline at end of file +}; diff --git a/app/s3browser/src/lib/api-client.ts b/app/s3browser/src/lib/api-client.ts index ffc5467..7a6ffc4 100644 --- a/app/s3browser/src/lib/api-client.ts +++ b/app/s3browser/src/lib/api-client.ts @@ -13,9 +13,7 @@ const isErrorData = (data: unknown): data is ErrorData => typeof (data as Record).error === 'string'; // Attempt to parse the response body as JSON -const parseErrorBody = async ( - response: Response, -): Promise => { +const parseErrorBody = async (response: Response): Promise => { try { const json: unknown = await response.json(); return isErrorData(json) ? json : null; @@ -27,10 +25,7 @@ const parseErrorBody = async ( // Decide whether a toast should be shown for this particular failure. // This is more flexible than a simple `silent` boolean because it allows for suppressing toasts // for expected failure cases. -const shouldSilence = ( - silent: boolean | number[] | undefined, - status: number, -): boolean => { +const shouldSilence = (silent: boolean | number[] | undefined, status: number): boolean => { if (silent === true) return true; if (Array.isArray(silent)) return silent.includes(status); @@ -57,17 +52,14 @@ type RequestOptions = RequestInit & { silent?: boolean | number[]; }; -async function request( - endpoint: string, - options: RequestOptions = {}, -): Promise { +async function request(endpoint: string, options: RequestOptions = {}): Promise { const { silent, ...fetchOptions } = options; const response = await fetch(`${BASE_URL}${endpoint}`, { ...fetchOptions, headers: { 'Content-Type': 'application/json', - 'Accept': 'application/json', + Accept: 'application/json', ...fetchOptions?.headers, }, credentials: 'include', diff --git a/app/s3browser/src/lib/api-error.ts b/app/s3browser/src/lib/api-error.ts index b35bd0f..bd0f9dd 100644 --- a/app/s3browser/src/lib/api-error.ts +++ b/app/s3browser/src/lib/api-error.ts @@ -1,4 +1,4 @@ -import { ErrorData } from "@/types/api"; +import { ErrorData } from '@/types/api'; /** Structured error class for API responses outside the 2xx range. @@ -14,7 +14,7 @@ export class ApiError extends Error { public data: ErrorData | null, ) { super((data?.error ?? statusText) || `HTTP ${status}`); - this.name = "ApiError"; + this.name = 'ApiError'; } get isServerError(): boolean { diff --git a/app/s3browser/src/lib/auth.ts b/app/s3browser/src/lib/auth.ts index b7086f4..de3fa7c 100644 --- a/app/s3browser/src/lib/auth.ts +++ b/app/s3browser/src/lib/auth.ts @@ -1,18 +1,18 @@ -import { useQuery } from "@tanstack/react-query"; -import { api } from "@/lib/api-client"; -import { User } from "@/types/api"; +import { useQuery } from '@tanstack/react-query'; +import { api } from '@/lib/api-client'; +import { User } from '@/types/api'; -export const AUTH_QUERY_KEY = ["authenticated-user"]; +export const AUTH_QUERY_KEY = ['authenticated-user']; async function getCurrentUser(): Promise { try { - return api.get("/users/_self", { + return api.get('/users/_self', { // Unauthenticated users will be redirected to login page, // so we can treat this as a non-error case and avoid showing a toast notification. silent: true, }); } catch (error) { - console.error("Error fetching current user:", error); + console.error('Error fetching current user:', error); return null; } } diff --git a/app/s3browser/src/lib/react-query.ts b/app/s3browser/src/lib/react-query.ts index 65d9b68..f7bbd5a 100644 --- a/app/s3browser/src/lib/react-query.ts +++ b/app/s3browser/src/lib/react-query.ts @@ -9,8 +9,9 @@ export const queryConfig = { }, } satisfies DefaultOptions; -export type ApiFnReturnType Promise> = - Awaited>; +export type ApiFnReturnType Promise> = Awaited< + ReturnType +>; // Extracts all React Query options (like enabled, onSuccess, etc.) EXCEPT queryKey and queryFn export type QueryConfig any> = Omit< @@ -18,10 +19,5 @@ export type QueryConfig any> = Omit< 'queryKey' | 'queryFn' >; -export type MutationConfig< - MutationFnType extends (...args: any) => Promise, -> = UseMutationOptions< - ApiFnReturnType, - Error, - Parameters[0] ->; +export type MutationConfig Promise> = + UseMutationOptions, Error, Parameters[0]>; diff --git a/app/s3browser/src/stores/notifications-store.ts b/app/s3browser/src/stores/notifications-store.ts index b1ccf15..3263a01 100644 --- a/app/s3browser/src/stores/notifications-store.ts +++ b/app/s3browser/src/stores/notifications-store.ts @@ -14,9 +14,9 @@ type NotificationsStore = { }; /** - * Use this hook to manage global notifications across the app. Generally, we prefer to use alerts closer - * to the source of the event (e.g. within a form or component), but this store is useful for triggering notifications - * from non-component code (e.g. utility functions, API clients) or for displaying messages when the form or component is unmounted + * Use this hook to manage global notifications across the app. Generally, we prefer to use alerts closer + * to the source of the event (e.g. within a form or component), but this store is useful for triggering notifications + * from non-component code (e.g. utility functions, API clients) or for displaying messages when the form or component is unmounted * (e.g. successful deletion of a resource that triggers a redirect). */ export const useNotifications = create((set) => ({ @@ -29,4 +29,4 @@ export const useNotifications = create((set) => ({ set((state) => ({ notifications: state.notifications.filter((n) => n.id !== id), })), -})); \ No newline at end of file +})); diff --git a/app/s3browser/src/types/api.ts b/app/s3browser/src/types/api.ts index 7f94a3b..4dcdc42 100644 --- a/app/s3browser/src/types/api.ts +++ b/app/s3browser/src/types/api.ts @@ -34,18 +34,18 @@ export type ObjectDetails = { // Each item is either a folder or an object, distinguished by the `type` field. export type BucketItem = | { - type: 'folder'; - name: string; - fullPath: string; - } + type: 'folder'; + name: string; + fullPath: string; + } | { - type: 'object'; - name: string; - fullPath: string; - size: number; - lastModified: string; - storageClass: string; - }; + type: 'object'; + name: string; + fullPath: string; + size: number; + lastModified: string; + storageClass: string; + }; export type User = { uid: string; @@ -54,4 +54,4 @@ export type User = { export type ErrorData = { error: string; -}; \ No newline at end of file +}; diff --git a/eslint.config.js b/eslint.config.js index 303ab20..417a5d4 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -3,10 +3,11 @@ import tseslint from 'typescript-eslint'; import react from 'eslint-plugin-react'; import reactHooks from 'eslint-plugin-react-hooks'; import globals from 'globals'; +import prettierConfig from 'eslint-config-prettier'; export default tseslint.config( { - ignores: ['node_modules/**', 'public/**', 'tmp/**', 'log/**', 'vendor/**'] + ignores: ['node_modules/**', 'public/**', 'tmp/**', 'log/**', 'vendor/**'], }, js.configs.recommended, { @@ -50,5 +51,7 @@ export default tseslint.config( rules: { 'react-hooks/incompatible-library': 'off', }, - } -); \ No newline at end of file + }, + // Disable formatting rules that conflict with Prettier + prettierConfig, +); diff --git a/lib/tasks/atc/ci.rake b/lib/tasks/atc/ci.rake index 328607a..d9ed46f 100644 --- a/lib/tasks/atc/ci.rake +++ b/lib/tasks/atc/ci.rake @@ -20,7 +20,7 @@ namespace :atc do task ci_nocop: ['atc:setup:config_files', :environment, 'atc:ci_specs'] desc 'CI build with Rubocop validation' - task ci: ['atc:setup:config_files', :environment, 'atc:rubocop', 'atc:eslint', 'atc:ci_specs'] + task ci: ['atc:setup:config_files', :environment, 'atc:rubocop', 'atc:eslint', 'atc:prettier', 'atc:ci_specs'] desc 'CI build just running specs' task ci_specs: :environment do @@ -50,7 +50,7 @@ namespace :atc do desc 'Run ESLint on S3 Browser Application code' task :eslint do - success = system('yarn eslint app/s3browser') + success = system('yarn lint') unless success puts 'ESLint found errors. Fix before committing.' @@ -58,6 +58,15 @@ namespace :atc do end end + desc 'Run Prettier formatting check on frontend code' + task :prettier do + success = system('yarn format:check') + unless success + puts 'Prettier found formatting issues. Run `yarn format` to fix.' + exit 1 + end + end + rescue LoadError => e # Be prepared to rescue so that this rake file can exist in environments where RSpec is unavailable (i.e. production environments). puts '[Warning] Exception creating ci/rubocop/rspec rake tasks. '\ diff --git a/package.json b/package.json index ab164cf..cc1a04f 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,10 @@ "type": "module", "scripts": { "start:dev": "./bin/vite dev", - "lint": "eslint app/s3browser", - "lint:fix": "eslint app/s3browser --fix" + "lint": "eslint app/s3browser app/javascript/entrypoints", + "lint:fix": "eslint app/s3browser app/javascript/entrypoints --fix", + "format": "prettier --write app/s3browser app/javascript/entrypoints", + "format:check": "prettier --check app/s3browser app/javascript/entrypoints" }, "packageManager": "yarn@4.14.1", "dependencies": { @@ -37,9 +39,11 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", "eslint": "^9.39.2", + "eslint-config-prettier": "^10.1.8", "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^7.0.1", "globals": "^17.6.0", + "prettier": "3.8.3", "typescript": "^5.9.3", "typescript-eslint": "^8.53.1", "vite": "^8.0.0", @@ -48,4 +52,4 @@ "browserslist": [ "defaults" ] -} +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 1097cdf..603bb59 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1219,9 +1219,11 @@ __metadata: "@vitejs/plugin-react": "npm:^6.0.1" bootstrap: "npm:^5.3.8" eslint: "npm:^9.39.2" + eslint-config-prettier: "npm:^10.1.8" eslint-plugin-react: "npm:^7.37.5" eslint-plugin-react-hooks: "npm:^7.0.1" globals: "npm:^17.6.0" + prettier: "npm:3.8.3" react: "npm:19.2.3" react-bootstrap: "npm:^2.10.10" react-dom: "npm:19.2.3" @@ -1861,6 +1863,17 @@ __metadata: languageName: node linkType: hard +"eslint-config-prettier@npm:^10.1.8": + version: 10.1.8 + resolution: "eslint-config-prettier@npm:10.1.8" + peerDependencies: + eslint: ">=7.0.0" + bin: + eslint-config-prettier: bin/cli.js + checksum: 10c0/e1bcfadc9eccd526c240056b1e59c5cd26544fe59feb85f38f4f1f116caed96aea0b3b87868e68b3099e55caaac3f2e5b9f58110f85db893e83a332751192682 + languageName: node + linkType: hard + "eslint-plugin-react-hooks@npm:^7.0.1": version: 7.1.1 resolution: "eslint-plugin-react-hooks@npm:7.1.1" @@ -3218,6 +3231,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:3.8.3": + version: 3.8.3 + resolution: "prettier@npm:3.8.3" + bin: + prettier: bin/prettier.cjs + checksum: 10c0/754816fd7593eb80f6376d7476d463e832c38a12f32775a82683adb6e35b772b1f484d65f19401507b983a8c8a7cd5a4a9f12006bd56491e8f35503473f77473 + languageName: node + linkType: hard + "proc-log@npm:^6.0.0": version: 6.1.0 resolution: "proc-log@npm:6.1.0"