-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add RJST (React JSON Schema Table) component
Change-type: minor
- Loading branch information
Showing
72 changed files
with
9,162 additions
and
115 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import type { CheckedState } from '../components/Table/utils'; | ||
import type { RJSTAction } from '../schemaOps'; | ||
import { useQuery } from 'react-query'; | ||
|
||
export const LOADING_DISABLED_REASON = 'Loading'; | ||
|
||
interface ActionContentProps<T> { | ||
action: RJSTAction<T>; | ||
affectedEntries: T[] | undefined; | ||
checkedState?: CheckedState; | ||
getDisabledReason: RJSTAction<T>['isDisabled']; | ||
onDisabledReady: (arg: string | null) => void; | ||
} | ||
|
||
// This component sole purpose is to have the useQuery being called exactly once per item, | ||
// so that it satisfies React hooks assumption that the number of hook calls inside each component | ||
// stays the same across renders. | ||
export const ActionContent = <T extends object>({ | ||
action, | ||
children, | ||
affectedEntries, | ||
checkedState, | ||
getDisabledReason, | ||
onDisabledReady, | ||
}: React.PropsWithChildren<ActionContentProps<T>>) => { | ||
useQuery({ | ||
queryKey: ['actionContent', action.title, affectedEntries, checkedState], | ||
queryFn: async () => { | ||
const disabled = | ||
(await getDisabledReason?.({ | ||
affectedEntries, | ||
checkedState, | ||
})) ?? null; | ||
onDisabledReady(disabled); | ||
return disabled; | ||
}, | ||
}); | ||
return children; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import React from 'react'; | ||
import type { | ||
ActionData, | ||
RJSTContext, | ||
RJSTModel, | ||
RJSTBaseResource, | ||
} from '../schemaOps'; | ||
import { rjstJsonSchemaPick } from '../schemaOps'; | ||
import { getCreateDisabledReason } from '../utils'; | ||
import { ActionContent, LOADING_DISABLED_REASON } from './ActionContent'; | ||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; | ||
import { faMagic } from '@fortawesome/free-solid-svg-icons/faMagic'; | ||
import { Box, Button } from '@mui/material'; | ||
import { Spinner } from '../../Spinner'; | ||
import { useTranslation } from '../../../hooks/useTranslations'; | ||
import { Tooltip } from '../../Tooltip'; | ||
|
||
interface CreateProps<T extends RJSTBaseResource<T>> { | ||
model: RJSTModel<T>; | ||
rjstContext: RJSTContext<T>; | ||
hasOngoingAction: boolean; | ||
onActionTriggered: (data: ActionData<T>) => void; | ||
} | ||
|
||
export const Create = <T extends RJSTBaseResource<T>>({ | ||
model, | ||
rjstContext, | ||
hasOngoingAction, | ||
onActionTriggered, | ||
}: CreateProps<T>) => { | ||
const { t } = useTranslation(); | ||
const { actions } = rjstContext; | ||
const createActions = actions?.filter((a) => a.type === 'create'); | ||
const [disabledReasonsByAction, setDisabledReasonsByAction] = | ||
React.useState<Record<string, string | undefined | null>>(); | ||
const [isInitialized, setIsInitialized] = React.useState(false); | ||
|
||
React.useEffect(() => { | ||
if (!isInitialized && createActions) { | ||
setDisabledReasonsByAction( | ||
Object.fromEntries( | ||
createActions.map((a) => [a.title, LOADING_DISABLED_REASON]), | ||
), | ||
); | ||
setIsInitialized(true); | ||
} | ||
}, [createActions, isInitialized]); | ||
|
||
if (!createActions || createActions.length < 1 || !disabledReasonsByAction) { | ||
return null; | ||
} | ||
|
||
if (createActions.length > 1) { | ||
throw new Error('Only one create action per resource is allowed'); | ||
} | ||
|
||
const [action] = createActions; | ||
|
||
const disabledReason = | ||
getCreateDisabledReason(model.permissions, hasOngoingAction, t) ?? | ||
disabledReasonsByAction[action.title]; | ||
return ( | ||
<Box display="flex"> | ||
<Tooltip | ||
title={typeof disabledReason === 'string' ? disabledReason : undefined} | ||
> | ||
<Button | ||
data-action={`create-${model.resource}`} | ||
variant="contained" | ||
onClick={() => { | ||
onActionTriggered({ | ||
action, | ||
schema: rjstJsonSchemaPick( | ||
model.schema, | ||
model.permissions.create, | ||
), | ||
}); | ||
}} | ||
startIcon={<FontAwesomeIcon icon={faMagic} />} | ||
disabled={!!disabledReason} | ||
> | ||
<ActionContent<T> | ||
action={action} | ||
getDisabledReason={action.isDisabled} | ||
affectedEntries={undefined} | ||
checkedState={undefined} | ||
onDisabledReady={(result) => { | ||
setDisabledReasonsByAction((disabledReasonsState) => ({ | ||
...disabledReasonsState, | ||
[action.title]: result, | ||
})); | ||
}} | ||
> | ||
<Box display="flex" justifyContent="space-between"> | ||
{action.title} | ||
<Spinner | ||
sx={{ ml: 2 }} | ||
show={disabledReason === LOADING_DISABLED_REASON} | ||
/> | ||
</Box> | ||
</ActionContent> | ||
</Button> | ||
</Tooltip> | ||
</Box> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import React from 'react'; | ||
import type { JSONSchema7 as JSONSchema } from 'json-schema'; | ||
import type { RJSTContext, RJSTBaseResource } from '../schemaOps'; | ||
import { parseDescriptionProperty } from '../schemaOps'; | ||
import get from 'lodash/get'; | ||
import { useTranslation } from '../../../hooks/useTranslations'; | ||
import type { | ||
ResourceTagSubmitInfo, | ||
SubmitInfo, | ||
} from '../../TagManagementDialog/models'; | ||
import { closeSnackbar, enqueueSnackbar } from 'notistack'; | ||
import { useQuery } from 'react-query'; | ||
import { Spinner } from '../../Spinner'; | ||
import { TagManagementDialog } from '../../TagManagementDialog'; | ||
|
||
interface TagsProps<T> { | ||
selected: T[] | undefined; | ||
rjstContext: RJSTContext<T>; | ||
schema: JSONSchema; | ||
setIsBusyMessage: (message: string | undefined) => void; | ||
onDone: () => void; | ||
refresh?: () => void; | ||
} | ||
|
||
export const Tags = <T extends RJSTBaseResource<T>>({ | ||
selected, | ||
rjstContext, | ||
schema, | ||
setIsBusyMessage, | ||
refresh, | ||
onDone, | ||
}: TagsProps<T>) => { | ||
const { t } = useTranslation(); | ||
|
||
const { sdk, internalPineFilter, checkedState } = rjstContext; | ||
|
||
const getAllTags = sdk?.tags && 'getAll' in sdk.tags ? sdk.tags.getAll : null; | ||
|
||
// This will get nested property names based on the x-ref-scheme property. | ||
const getItemName = (item: T) => { | ||
const property = schema.properties?.[ | ||
rjstContext.nameField as keyof typeof schema.properties | ||
] as JSONSchema; | ||
const refScheme = parseDescriptionProperty(property, 'x-ref-scheme'); | ||
|
||
if (refScheme != null && typeof refScheme === 'object') { | ||
const field = refScheme[0]; | ||
const nameFieldItem = item[rjstContext.nameField as keyof T]; | ||
return get( | ||
property.type === 'array' | ||
? (nameFieldItem as Array<T[keyof T]>)?.[0] | ||
: nameFieldItem, | ||
field, | ||
); | ||
} | ||
|
||
return item[rjstContext.nameField as keyof T]; | ||
}; | ||
|
||
const { data: items, isLoading } = useQuery({ | ||
queryKey: [ | ||
'tableTags', | ||
internalPineFilter, | ||
checkedState, | ||
getAllTags, | ||
selected == null, | ||
], | ||
queryFn: async () => { | ||
if ( | ||
// we are in server side pagination | ||
selected == null && | ||
checkedState === 'all' && | ||
getAllTags | ||
) { | ||
return (await getAllTags(internalPineFilter)) ?? null; | ||
} | ||
return selected ?? null; | ||
}, | ||
}); | ||
|
||
const changeTags = React.useCallback( | ||
async (tags: SubmitInfo<ResourceTagSubmitInfo, ResourceTagSubmitInfo>) => { | ||
if (!sdk?.tags) { | ||
return; | ||
} | ||
|
||
setIsBusyMessage(t(`loading.updating_tags`)); | ||
enqueueSnackbar({ | ||
key: 'change-tags-loading', | ||
message: t(`loading.updating_tags`), | ||
preventDuplicate: true, | ||
}); | ||
|
||
try { | ||
await sdk.tags.submit(tags); | ||
enqueueSnackbar({ | ||
key: 'change-tags', | ||
message: t('success.tags_updated_successfully'), | ||
variant: 'success', | ||
preventDuplicate: true, | ||
}); | ||
refresh?.(); | ||
} catch (err: any) { | ||
enqueueSnackbar({ | ||
key: 'change-tags', | ||
message: err.message, | ||
variant: 'error', | ||
preventDuplicate: true, | ||
}); | ||
} finally { | ||
closeSnackbar('change-tags-loading'); | ||
setIsBusyMessage(undefined); | ||
} | ||
}, | ||
[sdk?.tags, refresh, setIsBusyMessage, t], | ||
); | ||
|
||
if (!rjstContext.tagField || !rjstContext.nameField || !items) { | ||
return null; | ||
} | ||
|
||
return ( | ||
<Spinner show={isLoading} sx={{ width: '100%', height: '100%' }}> | ||
<TagManagementDialog<T> | ||
items={items} | ||
itemType={rjstContext.resource} | ||
titleField={getItemName ?? (rjstContext.nameField as keyof T)} | ||
tagField={rjstContext.tagField as keyof T} | ||
done={async (tagSubmitInfo) => { | ||
await changeTags(tagSubmitInfo); | ||
onDone(); | ||
}} | ||
cancel={() => { | ||
onDone(); | ||
}} | ||
/> | ||
</Spinner> | ||
); | ||
}; |
Oops, something went wrong.