diff --git a/cypress/e2e/dashboard.cy.ts b/cypress/e2e/dashboard.cy.ts index 286cb91..ff27368 100644 --- a/cypress/e2e/dashboard.cy.ts +++ b/cypress/e2e/dashboard.cy.ts @@ -5,18 +5,18 @@ describe('template spec', () => { cy.intercept( { method: 'GET', - url: `${PATHS.CELL}`, + url: `${PATHS.Cell}`, }, - { fixture: PATHS.CELL.replace(/^\//, '') }, + { fixture: PATHS.Cell.replace(/^\//, '') }, ) cy.intercept( { method: 'GET', - url: `${PATHS.CELL}/*`, + url: `${PATHS.Cell}/*`, }, async (req) => { const id = req.url.split('/').pop() - const cells = await cy.fixture(PATHS.CELL.replace(/^\//, '')) + const cells = await cy.fixture(PATHS.Cell.replace(/^\//, '')) req.reply({ body: cells.results.find((cell) => cell.uuid === id), }) diff --git a/src/App.tsx b/src/App.tsx index b7a8110..a8d75be 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -118,7 +118,7 @@ export function Core() { {open && 'Outputs'} - {[LOOKUP_KEYS.EXPERIMENT, LOOKUP_KEYS.CYCLER_TEST].map( + {[LOOKUP_KEYS.Experiment, LOOKUP_KEYS.CyclerTest].map( (lookupKey) => (
  • ), @@ -127,11 +127,11 @@ export function Core() { {open && 'Resources'} {[ - LOOKUP_KEYS.FILE, - LOOKUP_KEYS.CELL, - LOOKUP_KEYS.EQUIPMENT, - LOOKUP_KEYS.SCHEDULE, - LOOKUP_KEYS.ARBITRARY_FILE, + LOOKUP_KEYS.File, + LOOKUP_KEYS.Cell, + LOOKUP_KEYS.Equipment, + LOOKUP_KEYS.Schedule, + LOOKUP_KEYS.ArbitraryFile, ].map((lookupKey) => (
  • ))} @@ -139,10 +139,10 @@ export function Core() { {open && 'Inputs'} {[ - LOOKUP_KEYS.PATH, - LOOKUP_KEYS.VALIDATION_SCHEMA, - LOOKUP_KEYS.COLUMN_FAMILY, - LOOKUP_KEYS.UNIT, + LOOKUP_KEYS.Path, + LOOKUP_KEYS.ValidationSchema, + LOOKUP_KEYS.ColumnFamily, + LOOKUP_KEYS.Unit, ].map((lookupKey) => (
  • ))} @@ -150,10 +150,10 @@ export function Core() { {open && 'Management'} {[ - LOOKUP_KEYS.LAB, - LOOKUP_KEYS.TEAM, - LOOKUP_KEYS.HARVESTER, - LOOKUP_KEYS.ADDITIONAL_STORAGE, + LOOKUP_KEYS.Lab, + LOOKUP_KEYS.Team, + LOOKUP_KEYS.Harvester, + LOOKUP_KEYS.AdditionalStorage, ].map((lookupKey) => (
  • ))} @@ -275,7 +275,7 @@ export function Core() { return ( @@ -292,7 +292,7 @@ export function Core() { return <> } - return + return } /* A looks through its children s and renders the first one that matches the current URL. */ @@ -302,7 +302,7 @@ export function Core() { {/*} />*/} } /> {/* Handle direct resource lookups */} diff --git a/src/ClientCodeDemo.tsx b/src/ClientCodeDemo.tsx index 7d80682..34e749e 100644 --- a/src/ClientCodeDemo.tsx +++ b/src/ClientCodeDemo.tsx @@ -35,7 +35,7 @@ function DatasetSelector({ fileQueryLimit?: number }) { const { useListQuery } = useFetchResource() - const query = useListQuery(LOOKUP_KEYS.FILE, { + const query = useListQuery(LOOKUP_KEYS.File, { limit: fileQueryLimit ?? DEFAULT_FETCH_LIMIT, }) diff --git a/src/Components/ApiResourceContext.tsx b/src/Components/ApiResourceContext.tsx index 8ff3dbb..2598adf 100644 --- a/src/Components/ApiResourceContext.tsx +++ b/src/Components/ApiResourceContext.tsx @@ -46,10 +46,14 @@ export const get_select_function = (lookupKey: LookupKey) => (data: AxiosResponse) => { Object.entries(FIELDS[lookupKey]).forEach(([k, v]) => { - if (has(v, 'transformation')) + if (has(v, 'transformation')) { + console.warn( + `[FIELDS deprecation] ${lookupKey}.${k} has a transformation`, + ) data.data[k as keyof typeof data.data] = v.transformation( data.data[k as keyof typeof data.data], ) + } }) return data as AxiosResponse } diff --git a/src/Components/AttachmentUploadContext.tsx b/src/Components/AttachmentUploadContext.tsx index d2d4521..6dd289c 100644 --- a/src/Components/AttachmentUploadContext.tsx +++ b/src/Components/AttachmentUploadContext.tsx @@ -58,11 +58,11 @@ export default function AttachmentUploadContextProvider({ }, onSuccess: async (data: AxiosResponse) => { queryClient.setQueryData( - [LOOKUP_KEYS.ARBITRARY_FILE, data.data.id], + [LOOKUP_KEYS.ArbitraryFile, data.data.id], data.data, ) await queryClient.invalidateQueries({ - queryKey: [LOOKUP_KEYS.ARBITRARY_FILE, 'list'], + queryKey: [LOOKUP_KEYS.ArbitraryFile, 'list'], }) callback(data.data.url) }, diff --git a/src/Components/AuthImage.tsx b/src/Components/AuthImage.tsx index 3578864..af7fe61 100644 --- a/src/Components/AuthImage.tsx +++ b/src/Components/AuthImage.tsx @@ -30,7 +30,7 @@ export default function AuthImage({ 'Galv-Storage-No-Redirect': true, } const query = useQuery({ - queryKey: [LOOKUP_KEYS.FILE, file.id, 'png'], + queryKey: [LOOKUP_KEYS.File, file.id, 'png'], queryFn: async () => { const response = await axios.get(file.png, { headers, diff --git a/src/Components/CardActionBar.tsx b/src/Components/CardActionBar.tsx index e031e60..1ff58d3 100644 --- a/src/Components/CardActionBar.tsx +++ b/src/Components/CardActionBar.tsx @@ -32,7 +32,7 @@ import { has_value, id_from_ref_props } from './misc' import clsx from 'clsx' import UseStyles from '../styles/UseStyles' import { useSelectionManagement } from './SelectionManagementContext' -import { representation } from './Representation' +import { genericRepresentation } from './representation/GenericRepresentation' import SafeTooltip from './SafeTooltip' export type CardActionBarProps = { @@ -67,17 +67,33 @@ export type CardActionBarProps = { */ export default function CardActionBar(props: CardActionBarProps) { const { classes, theme } = UseStyles() - const { apiResource } = useApiResource() + const { apiResource, apiResourceDescription } = useApiResource() const iconProps: Partial = { ...props.iconProps, } const selectable = props.selectable ?? typeof apiResource?.id === 'string' const { toggleSelected, isSelected } = useSelectionManagement() + // Check differences between FIELDS context and apiResourceDescription context + const api_context = Object.values(apiResourceDescription ?? {}).filter( + (e) => e.galv_resource, + ) + const fields_context = Object.values(FIELDS[props.lookupKey]).filter((e) => + is_lookupKey(e.type), + ) + // const api_only = api_context.filter((e) => !fields_context.includes(e)) + const fields_only = fields_context.filter((e) => !api_context.includes(e)) + if (fields_only.length > 0) { + console.warn( + `[FIELDS deprecation] FIELD[${props.lookupKey}] has fields not present in apiResourceDescription.`, + fields_only, + ) + } + const context_section = ( <> - {Object.entries(FIELDS[props.lookupKey]) - .filter((e) => is_lookupKey(e[1].type)) + {Object.entries(apiResourceDescription ?? {}) + .filter((e) => e[1].galv_resource) .map(([k, v]) => { const relative_lookupKey = v.type as LookupKey let content: ReactNode @@ -225,7 +241,7 @@ export default function CardActionBar(props: CardActionBarProps) { const destroy_section = ( - {props.lookupKey === LOOKUP_KEYS.FILE && props.reimportable && ( + {props.lookupKey === LOOKUP_KEYS.File && props.reimportable && ( { if (!user) return - const api = new API_HANDLERS[LOOKUP_KEYS.USER](api_config) + const api = new API_HANDLERS[LOOKUP_KEYS.User](api_config) return api .usersRetrieve({ id: user.id }) .then((response: AxiosResponse) => { diff --git a/src/Components/FetchResourceContext.tsx b/src/Components/FetchResourceContext.tsx index 4be5aa8..01e791a 100644 --- a/src/Components/FetchResourceContext.tsx +++ b/src/Components/FetchResourceContext.tsx @@ -54,7 +54,8 @@ export type FieldDescription = { | 'choice' | 'json' | string - many: boolean + galv_resource: boolean // Whether the `type` is a Galv Resource + many: boolean // Whether the field is a list help_text: string required: boolean read_only: boolean @@ -558,7 +559,7 @@ export default function FetchResourceContextProvider({ queryClient.invalidateQueries({ queryKey: [lookupKey, 'list'], }) - if (lookupKey === LOOKUP_KEYS.LAB) { + if (lookupKey === LOOKUP_KEYS.Lab) { refresh_user() } // Invalidate autocomplete cache diff --git a/src/Components/Mapping.tsx b/src/Components/Mapping.tsx index 3657ec5..fe40751 100644 --- a/src/Components/Mapping.tsx +++ b/src/Components/Mapping.tsx @@ -166,7 +166,7 @@ function CreateColumnType({ onCreate(new_resource_url) }} onDiscard={() => setOpen(false)} - lookupKey={LOOKUP_KEYS.COLUMN_FAMILY} + lookupKey={LOOKUP_KEYS.ColumnFamily} /> @@ -184,7 +184,7 @@ function SelectColumnType({ reset_name: string }) { const { useListQuery } = useFetchResource() - const query = useListQuery(LOOKUP_KEYS.COLUMN_FAMILY) + const query = useListQuery(LOOKUP_KEYS.ColumnFamily) const { classes } = useStyles() const [createModalOpen, setCreateModalOpen] = useState(false) @@ -826,7 +826,7 @@ function MappingManager({ }) const { classes } = useStyles() const { useListQuery, useCreateQuery, useUpdateQuery } = useFetchResource() - const col_query = useListQuery(LOOKUP_KEYS.COLUMN_FAMILY) + const col_query = useListQuery(LOOKUP_KEYS.ColumnFamily) const [more, setMore] = React.useState(false) const [advancedPropertiesOpen, setAdvancedPropertiesOpen] = React.useState(false) @@ -838,14 +838,14 @@ function MappingManager({ return m ? { ...m } : blank_map() }) const navigate = useNavigate() - const updateFileMutation = useUpdateQuery(LOOKUP_KEYS.FILE) + const updateFileMutation = useUpdateQuery(LOOKUP_KEYS.File) const updateFile = (new_mapping: DB_MappingResource) => updateFileMutation.mutate( { ...file!, mapping: new_mapping.url }, { onSuccess: () => navigate(0) }, ) const createMapMutation = useCreateQuery( - LOOKUP_KEYS.MAPPING, + LOOKUP_KEYS.ColumnMapping, { after_cache: (r) => updateFile(r.data) }, ) const createMap = (data: ApplicableMappingResource) => { @@ -864,12 +864,12 @@ function MappingManager({ }) } const updateMapMutation = useUpdateQuery( - LOOKUP_KEYS.MAPPING, + LOOKUP_KEYS.ColumnMapping, ) const updateMap = (data: DB_MappingResource) => updateMapMutation.mutate(data, { onSuccess: () => navigate(0) }) // TODO: Implement deleteMapMutation when backend supplies delete permissions - // const deleteMapMutation = useDeleteQuery(LOOKUP_KEYS.MAPPING) + // const deleteMapMutation = useDeleteQuery(LOOKUP_KEYS.ColumnMapping) // const deleteMap = (data: DB_MappingResource) => deleteMapMutation.mutate(data, {onSuccess: () => navigate(0)}) if (col_query?.hasNextPage && !col_query.isFetchingNextPage) @@ -963,7 +963,7 @@ function MappingManager({ {file?.id && ( )} @@ -1231,12 +1231,12 @@ Do you wish to continue?`) diff --git a/src/Components/ResourceChip.tsx b/src/Components/ResourceChip.tsx index adb15a1..3c5b3fd 100644 --- a/src/Components/ResourceChip.tsx +++ b/src/Components/ResourceChip.tsx @@ -8,7 +8,7 @@ import QueryWrapper, { QueryWrapperProps } from './QueryWrapper' import ErrorChip from './error/ErrorChip' import { FAMILY_LOOKUP_KEYS, GalvResource, PATHS } from '../constants' import ErrorBoundary from './ErrorBoundary' -import Representation from './Representation' +import Representation from './representation/GenericRepresentation' import { FilterContext } from './filtering/FilterContext' import ApiResourceContextProvider, { ApiResourceContextProviderProps, diff --git a/src/Components/ResourceCreator.tsx b/src/Components/ResourceCreator.tsx index af849d3..90df114 100644 --- a/src/Components/ResourceCreator.tsx +++ b/src/Components/ResourceCreator.tsx @@ -109,7 +109,7 @@ export function TokenCreator({ weeks: 60 * 60 * 24 * 7, } - const lookupKey = LOOKUP_KEYS.TOKEN + const lookupKey = LOOKUP_KEYS.Token const ICON = ICONS[lookupKey] @@ -364,7 +364,7 @@ export function ResourceCreator({ ) const handleSave = () => { - if (lookupKey === LOOKUP_KEYS.ARBITRARY_FILE) { + if (lookupKey === LOOKUP_KEYS.ArbitraryFile) { create_attachment_mutation.mutate({ ...clean(UndoRedo.current), file, @@ -492,28 +492,45 @@ export default function WrappedResourceCreator( const [modalOpen, setModalOpen] = useState(false) const { user, refresh_user } = useCurrentUser() const navigate = useNavigate() + const { useDescribeQuery } = useFetchResource() + const description_query = useDescribeQuery(props.lookupKey) + const [createable, setCreateable] = useState(false) - const get_can_create = (lookupKey: LookupKey) => { + useEffect(() => { // We can always create tokens because they represent us, and labs because someone has to. - if (lookupKey === LOOKUP_KEYS.TOKEN) return !!user - if (lookupKey === LOOKUP_KEYS.LAB) return !!user + if ( + props.lookupKey === LOOKUP_KEYS.Token || + props.lookupKey === LOOKUP_KEYS.Lab + ) { + setCreateable(!!user) + return + } const lab_admin_resources = [ - LOOKUP_KEYS.TEAM, - LOOKUP_KEYS.ADDITIONAL_STORAGE, + LOOKUP_KEYS.Team, + LOOKUP_KEYS.AdditionalStorage, ] as LookupKey[] - if (lab_admin_resources.includes(lookupKey)) return user?.is_lab_admin + if (lab_admin_resources.includes(props.lookupKey)) { + setCreateable(!!user?.is_lab_admin) + return + } - const fields = FIELDS[lookupKey] - return Object.keys(fields).includes('team') - } + console.log(`Checking if ${props.lookupKey} can be created`, { + description_query_data: description_query.data, + }) + + setCreateable( + !!description_query.data?.data && + Object.keys(description_query.data.data).includes('team'), + ) + }, [description_query.data, props.lookupKey, user]) - if (!get_can_create(props.lookupKey)) return <> + if (!createable) return <> const ADD_ICON = ICONS.CREATE // Files are a special case and there's a whole page for handling them - if (props.lookupKey === LOOKUP_KEYS.FILE) + if (props.lookupKey === LOOKUP_KEYS.File) return (