/** * Contains the base function that handles read, view, and entry all of them * in a single function single the logic that handles this under the hood is remarkably similar * * @module */ import React, { useContext } from "react"; import { IPropertyDefinitionState } from "../../../base/Root/Module/ItemDefinition/PropertyDefinition"; import { PropertyDefinitionSupportedType } from "../../../base/Root/Module/ItemDefinition/PropertyDefinition/types"; import { IItemContextType, ItemContext } from "../../providers/item"; import { PropertyDefinitionSearchInterfacesPrefixes } from "../../../base/Root/Module/ItemDefinition/PropertyDefinition/search-interfaces"; import { RESERVED_BASE_PROPERTIES_RQ, SearchVariants } from "../../../constants"; import PropertyView, { RawBasePropertyView } from "../../internal/components/PropertyView"; import PropertyEntry from "../../internal/components/PropertyEntry"; import PropertySetter from "../../internal/components/PropertySetter"; import { fileURLAbsoluter, fileArrayURLAbsoluter } from "../../../util"; import { IRQFile } from "../../../rq-querier"; import { ConfigContext } from "../../internal/providers/config-provider"; import equals from "deep-equal"; export interface IPropertyCoreProps { /** * the id of the property that must exist under the item definition * provider */ id: T; /** * A search variant, exact, from, to, radius, etc... * only truly available in search mode */ searchVariant?: SV extends never ? undefined : SV; } /** * The base interface, all entry, read, view, set contain these attributes */ export interface IPropertyBaseProps extends IPropertyCoreProps { /** * the policy type, should be provided with a policy name */ policyType?: string; /** * the policy name, should be provided with a policy type */ policyName?: string; /** * an include if provided */ include?: string; } export interface IPropertyBasePropsWithItemContext extends IPropertyBaseProps { /** * Specify a context if not given */ context?: IItemContextType; } /** * The base interface with renderers this time */ export interface IPropertyBaseWithRendererProps extends IPropertyBasePropsWithItemContext { /** * the renderer in charge of rendering the output */ renderer?: React.ComponentType; /** * Extra renderer args */ rendererArgs?: object; } /** * The entry props for all read, view, and entry */ export interface IPropertyEntryProps extends IPropertyBaseWithRendererProps { /** * an optional function to get the value as the property changes * these changes are given by the entry and do not come when * the property change due to an external force */ onEntryDrivenChange?: (value: PropertyDefinitionSupportedType) => void; /** * make it seem as invalid, allows displaying an entry property as invalid * make it a string to use a custom error message from the property */ showAsInvalid?: boolean | string; /** * Whether to hide the description that is hidden in language data */ hideDescription?: boolean; /** * An alternative description */ altDescription?: string; /** * An alternative label */ altLabel?: string; /** * Hide the label */ hideLabel?: boolean; /** * Hide the placeholder */ hidePlaceholder?: boolean; /** * An alternative placeholder */ altPlaceholder?: string; /** * Whether to ignore the errors * that are given */ ignoreErrors?: boolean; /** * Focus on mount */ autoFocus?: boolean; /** * The value to prefill with on mount */ prefillWith?: PropertyDefinitionSupportedType; /** * whether to cache files when running the url absoluter * * this uses the service worker mechanism, it will not affect * files that have been cached internally using long term * caching */ cacheFiles?: boolean; /** * force to be disabled */ disabled?: boolean; /** * Will display even if it's hidden */ displayHidden?: boolean; /** * Used for the text type allows to override the language * of a property for a new value */ languageOverride?: string; /** * Suppress hidden warning */ suppressWarnings?: boolean; /** * Handle as the given type subtype rather than * the default */ handleAs?: { type: string; subtype?: string; }; /** * uses the applied value to display * NOTE: this makes the entry basically readonly * as it can't update the value, use this only * in disabled entries */ useAppliedValue?: boolean; } /** * The setter props */ export interface IPropertySetterProps extends IPropertyBasePropsWithItemContext { /** * The value to provide to such property */ value: V; } /** * The reader props */ export interface IPropertyReadPropsWOChildren extends IPropertyBasePropsWithItemContext { /** * whether to use the applied value rather than the * actual current value the applied value is synced * with the server */ useAppliedValue?: boolean; /** * whether to cache files when running the url absoluter */ cacheFiles?: boolean; } /** * @ignore */ export type ReadSetterCallback = (v: T, internalValue: any) => void; /** * The reader props */ export interface IPropertyReadProps extends IPropertyReadPropsWOChildren { /** * The reader callback */ children?: (value: PDEF, state: IPropertyDefinitionState, setter: ReadSetterCallback) => React.ReactNode; } /** * The view props */ export interface IPropertyViewProps extends IPropertyBaseWithRendererProps { /** * Whether to capitalize */ capitalize?: boolean; /** * whether to use the applied value rather than the * actual current value the applied value is synced * with the server */ useAppliedValue?: boolean; /** * whether to cache files when running the url absoluter */ cacheFiles?: boolean; /** * Will display even if it's hidden */ displayHidden?: boolean; /** * Handle as the given type subtype rather than * the default */ handleAs?: { type: string; subtype?: string; }; /** * Normally values are displayed in the timezone of the client * rather than in the values that they are specified originally * in the original timezone, use this to ensure that the value is * displayed in the original timezone */ retainTimeZone?: boolean; } /** * The props it takes basically extends everything */ interface IPropertyEntryViewReadSetProps extends IPropertyEntryProps, IPropertyViewProps, IPropertySetterProps, IPropertyReadProps { } /** * This is a legit function that takes all the props in order to pipe them * to the proper handler * @param props the props that are passed * @param type the type, entry, view, read, or set * @returns a react component */ export function EntryViewReadSet( originalProps: IPropertyEntryViewReadSetProps | string, type: "entry" | "view" | "read" | "set", use?: boolean, ): any { const config = useContext(ConfigContext); const itemContextualValueFromContext = useContext(ItemContext); let props = originalProps as IPropertyEntryViewReadSetProps; if (typeof originalProps === "string" || originalProps === null) { props = { id: originalProps, } as any; } const itemContextualValue = props.context || itemContextualValueFromContext; // so we need to be in an item definition contextual value // because otherwise we just can't get the property we need if (!itemContextualValue) { throw new Error("The Entry/View/Read/Set must be in a ItemProvider context"); } const include = props.include ? itemContextualValue.idef.getIncludeFor(props.include) : null; // now we need the actual id, because search variants // cause another id let actualId = props.id; if (actualId === null) { if (type === "read") { if (use) { return [null, null]; } return props.children(null, null, null); } else { return null; } } if (props.searchVariant) { // for that we just get the prefix and add it actualId = PropertyDefinitionSearchInterfacesPrefixes[props.searchVariant.toUpperCase().replace("-", "_")] + props.id; } // now we need to check if this is a meta property such a created_at, edited_at, etc... const isMetaProperty = !!RESERVED_BASE_PROPERTIES_RQ[actualId]; // and once we know that we can get the value, basically we // don't have a property definition if is a meta property, and we need to extract // it from the include if it's available const property = !isMetaProperty ? ( include ? include.getSinkingPropertyFor(actualId) : ( (props.policyType && props.policyName) ? itemContextualValue.idef .getPropertyDefinitionForPolicy(props.policyType, props.policyName, actualId) : itemContextualValue.idef .getPropertyDefinitionFor(actualId, true) ) ) : null; // now we need to get the property state let propertyState: IPropertyDefinitionState = null; // if it's not a meta property we can access it if (!isMetaProperty) { // if we are into an include if (include) { // this might be null if the state is excluded, which makes the property state // be null and unknown const includeState = itemContextualValue.state.includes.find((i) => i.includeId === include.getId()); if (includeState.itemState) { propertyState = includeState.itemState.properties .find((p) => p.propertyId === actualId); } } else if (props.policyType && props.policyName) { // otherwise if we refer to a policy propertyState = itemContextualValue.state && itemContextualValue.state.policies && itemContextualValue.state.policies[props.policyType] && itemContextualValue.state.policies[props.policyType][props.policyName] && itemContextualValue.state.policies[props.policyType][props.policyName].find((p: IPropertyDefinitionState) => p.propertyId === actualId); } else { // otherwise propertyState = itemContextualValue.state.properties.find((p) => p.propertyId === actualId); } } // so now we can start in conditional rendering if (type === "read") { // so if we have a property state (which will not exist in meta properties) if (propertyState) { // we get the property description const propertyDescription = property.getPropertyDefinitionDescription(); const setter = property ? itemContextualValue.onPropertyChange.bind(null, property) : null; // to check if we are talking about file, or files if (propertyDescription.rqRepresentsFile) { // and if that is the case, now we need to check if it's a // list or a single value const domain = process.env.NODE_ENV === "production" ? config.productionHostname : config.developmentHostname; // and as such we absolute the files so that their urls // are indeed proper, being a read operation, there's no risk on it if (!propertyDescription.rq.array) { const value = fileURLAbsoluter( domain, config.defaultCluster, config.allClusters, props.useAppliedValue ? propertyState.stateAppliedValue as IRQFile : propertyState.value as IRQFile, itemContextualValue.idef, itemContextualValue.forId, itemContextualValue.forVersion, include, property, !!props.cacheFiles, ); if (use) { return [value, propertyState, setter]; } return props.children(value, propertyState, setter); } else { const value = fileArrayURLAbsoluter( domain, config.defaultCluster, config.allClusters, props.useAppliedValue ? propertyState.stateAppliedValue as IRQFile[] : propertyState.value as IRQFile[], itemContextualValue.idef, itemContextualValue.forId, itemContextualValue.forVersion, include, property, !!props.cacheFiles, ); if (use) { return [value, propertyState, setter]; } return props.children(value, propertyState, setter); } } if (use) { return [ props.useAppliedValue ? propertyState.stateAppliedValue : propertyState.value, propertyState, setter, ]; } // otherwise we just call the function as it is return props.children( props.useAppliedValue ? propertyState.stateAppliedValue : propertyState.value, propertyState, setter, ); } // if we are talking a meta property if (isMetaProperty) { let rqValue: any; if (actualId === "id") { rqValue = itemContextualValue.forId; } else if (actualId === "version") { rqValue = itemContextualValue.forVersion; } else { rqValue = itemContextualValue.state.rqOriginalFlattenedValue && itemContextualValue.state.rqOriginalFlattenedValue[actualId]; } // if it's undefined we give null if (typeof rqValue === "undefined") { rqValue = null; } // and then call the children if (use) { return [rqValue, null]; } return props.children(rqValue as any, null, null); } if (process.env.NODE_ENV === "development" && !props.suppressWarnings) { console.warn( "Possibly unwanted behaviour, you used a Reader on property " + property.getId() + " but it has not been loaded on this context, if you wish to ignore this, use suppressWarnings={true}" ); } if (use) { return [null, null]; } // Property has no state, and no internal value, it must be somehow hidden return null; // we go now for views } else if (type === "view") { // now in the case of these, we have special considerations if (propertyState) { if ( !props.displayHidden && propertyState.hidden && process.env.NODE_ENV === "development" && !props.suppressWarnings ) { console.warn( "You used a View on property " + property.getId() + " but it is currently hidden, if you want it to display it anyway use displayHidden={true}" ); } // if we have a state it's simple, and we pass these values into it return ( ); } // if we have a meta property nevertheless if (isMetaProperty) { // we need to read its value let rqValue = itemContextualValue.state.rqOriginalFlattenedValue && itemContextualValue.state.rqOriginalFlattenedValue[actualId]; if (typeof rqValue === "undefined") { rqValue = null; } // and then pass it to the raw base property view, which will // use the appropiate rendered for this if (typeof rqValue === "number") { return ; } else { return ; } } if (process.env.NODE_ENV === "development" && !props.suppressWarnings) { console.warn( "Possibly unwanted behaviour, you used a View on property " + property.getId() + " but it has not been loaded on this context, if you wish to ignore this use suppressWarnings={true}" ); } // otherwise there's nothing to display, the property // has no state, it must somehow be hidden or unavailable return null; // now for entries } else if (type === "entry") { // property has no state it must be hidden // or somehow not accessible eg. it has been optimized for if (!propertyState) { if (process.env.NODE_ENV === "development" && !props.suppressWarnings) { console.warn( "Possibly unwanted behaviour, you used a Entry on property " + property.getId() + " but it has not been loaded on this context, if you wisth to ignore this use suppressWarnings={true}" ); } return null; } if ( !props.displayHidden && propertyState.hidden && process.env.NODE_ENV === "development" && !props.suppressWarnings ) { console.warn( "Possibly unwanted behaviour, you used a Entry on property " + property.getId() + " but it is currently hidden, if you want it to display it anyway use displayHidden={true} or suppressWarnings={true}" ); } // this is the proper onchange function const onChange = (newValue: PropertyDefinitionSupportedType, internalValue?: any) => { let valueBeforeUpdate: PropertyDefinitionSupportedType; if (props.onEntryDrivenChange) { valueBeforeUpdate = property.getCurrentValue(itemContextualValue.forId, itemContextualValue.forVersion); } itemContextualValue.onPropertyChange(property, newValue, internalValue); if (props.onEntryDrivenChange && !equals(valueBeforeUpdate, newValue, { strict: true })) { props.onEntryDrivenChange(newValue); } }; // and the on restore function const onRestore = () => { let valueBeforeUpdate: PropertyDefinitionSupportedType; if (props.onEntryDrivenChange) { valueBeforeUpdate = property.getCurrentValue(itemContextualValue.forId, itemContextualValue.forVersion); } itemContextualValue.onPropertyRestore(property); if (props.onEntryDrivenChange) { const value = property.getCurrentValue(itemContextualValue.forId, itemContextualValue.forVersion); if (!equals(valueBeforeUpdate, value, { strict: true })) { props.onEntryDrivenChange(value); } } } // and now for whether it's poked, by default it's false let isPoked: boolean = false; // now we search for it, so for a policy we search in the pokedElements // to see if it's there if (props.policyType) { isPoked = !!itemContextualValue.pokedElements.policies.find( pElement => pElement[0] === props.policyType && pElement[1] === props.policyName && pElement[2] === property.getId()); } else if (include) { // for the includes we do something similar as well const pokeInclude = Object.keys(itemContextualValue.pokedElements.includes).find((iId) => { return iId === include.getId(); }); isPoked = pokeInclude && pokeInclude.includes(property.getId()); } else { // and for a specific property isPoked = !!itemContextualValue.pokedElements.properties.find((pId) => { return pId === property.getId(); }); } // and then we can return the property entry in all its glory return ( ); } else { // so we return the propery setter return ( ); } }