@strato-admin/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ import React, { ReactNode } from 'react';
2
+ import { Resource, ResourceProps } from '@strato-admin/ra-core';
3
+ import { ResourceSchemaProvider } from './ResourceSchemaProvider';
4
+ import { registerGlobalSchemas, useSchemaRegistry, getDefaultResourceComponents, parseUnifiedSchema } from './SchemaRegistry';
5
+
6
+ export interface ResourceSchemaProps extends ResourceProps {
7
+ fieldSchema?: ReactNode;
8
+ inputSchema?: ReactNode;
9
+ children?: ReactNode;
10
+ label?: string;
11
+ canCreate?: boolean;
12
+ canEdit?: boolean;
13
+ canDelete?: boolean;
14
+ canShowDetails?: boolean;
15
+ canList?: boolean;
16
+ }
17
+
18
+ /**
19
+ * A wrapper around React-Admin's <Resource> that allows defining
20
+ * the field and input schemas via children or props.
21
+ *
22
+ * @example
23
+ * <ResourceSchema name="posts" list={PostList}>
24
+ * <TextField source="title" />
25
+ * </ResourceSchema>
26
+ */
27
+ export const ResourceSchema = ({
28
+ fieldSchema: explicitFieldSchema,
29
+ inputSchema: explicitInputSchema,
30
+ children,
31
+ label,
32
+ options,
33
+ canCreate = true,
34
+ canEdit = true,
35
+ canDelete = true,
36
+ canShowDetails = true,
37
+ canList = true,
38
+ ...props
39
+ }: ResourceSchemaProps) => {
40
+ const { defaultComponents } = useSchemaRegistry();
41
+
42
+ const parsedSchemas = children ? parseUnifiedSchema(children) : {};
43
+ const fieldSchema = explicitFieldSchema || parsedSchemas.fieldSchema;
44
+ const inputSchema = explicitInputSchema || parsedSchemas.inputSchema;
45
+
46
+ const mergedOptions = {
47
+ ...options,
48
+ ...(label ? { label } : {}),
49
+ canCreate,
50
+ canEdit,
51
+ canDelete,
52
+ canShowDetails,
53
+ canList,
54
+ };
55
+
56
+ const finalProps = {
57
+ ...props,
58
+ list: canList ? (props.list || defaultComponents.list) : undefined,
59
+ create: canCreate ? (props.create || defaultComponents.create) : undefined,
60
+ edit: canEdit ? (props.edit || defaultComponents.edit) : undefined,
61
+ show: canShowDetails ? (props.show || defaultComponents.show) : undefined,
62
+ };
63
+
64
+ return (
65
+ <ResourceSchemaProvider resource={props.name} fieldSchema={fieldSchema} inputSchema={inputSchema}>
66
+ <Resource {...finalProps} options={mergedOptions} />
67
+ </ResourceSchemaProvider>
68
+ );
69
+ };
70
+
71
+ ResourceSchema.raName = 'Resource';
72
+
73
+ /**
74
+ * This is called by React-Admin during Admin initialization.
75
+ * We use it to register schemas globally before any component renders.
76
+ */
77
+ ResourceSchema.registerResource = (props: ResourceSchemaProps) => {
78
+ const {
79
+ name,
80
+ fieldSchema: explicitFieldSchema,
81
+ inputSchema: explicitInputSchema,
82
+ children,
83
+ label,
84
+ options,
85
+ canCreate = true,
86
+ canEdit = true,
87
+ canDelete = true,
88
+ canShowDetails = true,
89
+ canList = true,
90
+ list,
91
+ create,
92
+ edit,
93
+ show,
94
+ } = props;
95
+
96
+ const parsedSchemas = children ? parseUnifiedSchema(children) : {};
97
+ const fieldSchema = explicitFieldSchema || parsedSchemas.fieldSchema;
98
+ const inputSchema = explicitInputSchema || parsedSchemas.inputSchema;
99
+
100
+ if (name && (fieldSchema || inputSchema)) {
101
+ registerGlobalSchemas(name, { fieldSchema, inputSchema });
102
+ }
103
+
104
+ const defaultComponents = getDefaultResourceComponents();
105
+ const mergedOptions = {
106
+ ...options,
107
+ ...(label ? { label } : {}),
108
+ canCreate,
109
+ canEdit,
110
+ canDelete,
111
+ canShowDetails,
112
+ canList,
113
+ };
114
+
115
+ const finalProps = {
116
+ ...props,
117
+ list: canList ? (list || defaultComponents.list) : undefined,
118
+ create: canCreate ? (create || defaultComponents.create) : undefined,
119
+ edit: canEdit ? (edit || defaultComponents.edit) : undefined,
120
+ show: canShowDetails ? (show || defaultComponents.show) : undefined,
121
+ options: mergedOptions,
122
+ };
123
+
124
+ return (Resource as any).registerResource(finalProps);
125
+ };
@@ -0,0 +1,114 @@
1
+ import React, { ReactNode, Children, useMemo, useContext } from 'react';
2
+ import { FieldSchemaContext } from './FieldSchema';
3
+ import { InputSchemaContext } from './InputSchema';
4
+ import { useSchemaRegistry } from './SchemaRegistry';
5
+ import { ResourceContext } from './ResourceContext';
6
+
7
+ export interface ResourceSchemaProviderProps {
8
+ children: ReactNode;
9
+ resource?: string;
10
+ fieldSchema?: ReactNode;
11
+ inputSchema?: ReactNode;
12
+ }
13
+
14
+ /**
15
+ * Provides the field and input schemas to its children via context.
16
+ * This is a lightweight version of ResourceSchema that does not register
17
+ * a React-Admin <Resource> or define routes.
18
+ *
19
+ * It can also look up schemas in the central registry if a resource name is provided.
20
+ */
21
+ export const ResourceSchemaProvider = ({
22
+ children,
23
+ resource,
24
+ fieldSchema,
25
+ inputSchema,
26
+ }: ResourceSchemaProviderProps) => {
27
+ const { registerSchemas, getSchemas } = useSchemaRegistry();
28
+ const parentResource = useContext(ResourceContext);
29
+
30
+ // 1. Register schemas synchronously during render.
31
+ useMemo(() => {
32
+ if (resource && (fieldSchema || inputSchema)) {
33
+ registerSchemas(resource, { fieldSchema, inputSchema });
34
+ }
35
+ }, [resource, fieldSchema, inputSchema, registerSchemas]);
36
+
37
+ // 2. Lookup schemas if not provided
38
+ // We MUST check the registry if resource is provided, even if we have some schemas from props,
39
+ // because props might only contain ONE of them (e.g. only fieldSchema).
40
+ const registrySchemas = useMemo(() => {
41
+ const effectiveResource = resource || parentResource;
42
+ if (effectiveResource) {
43
+ return getSchemas(effectiveResource);
44
+ }
45
+ return undefined;
46
+ }, [resource, parentResource, getSchemas]);
47
+
48
+ const finalFieldSchema = fieldSchema || registrySchemas?.fieldSchema;
49
+ const finalInputSchema = inputSchema || registrySchemas?.inputSchema;
50
+
51
+ // Extract children if the schemas are passed as <FieldSchema> or <InputSchema> elements
52
+ const getChildren = (schema: ReactNode) => {
53
+ if (React.isValidElement(schema)) {
54
+ if ('children' in (schema.props as any)) {
55
+ return (schema.props as any).children;
56
+ }
57
+ }
58
+ return schema;
59
+ };
60
+
61
+ const fieldChildren = useMemo(() =>
62
+ finalFieldSchema ? Children.toArray(getChildren(finalFieldSchema)) : undefined
63
+ , [finalFieldSchema]);
64
+
65
+ const inputChildren = useMemo(() =>
66
+ finalInputSchema ? Children.toArray(getChildren(finalInputSchema)) : undefined
67
+ , [finalInputSchema]);
68
+
69
+ let content = children;
70
+
71
+ // 3. Wrap in new ResourceContext if the resource changed.
72
+ if (resource && resource !== parentResource) {
73
+ content = (
74
+ <ResourceContext.Provider value={resource}>
75
+ {content}
76
+ </ResourceContext.Provider>
77
+ );
78
+ }
79
+
80
+ // 4. Wrap in Schema Providers.
81
+ // We provide a new context if:
82
+ // - The resource changed (prevents leakage)
83
+ // - OR we have explicit schema children to provide
84
+ if (resource && resource !== parentResource) {
85
+ // If resource changed, we MUST provide a context to isolate children.
86
+ // If no schema found, we provide [] ONLY IF we are confident it's a known resource.
87
+ // Actually, providing [] is safer to block leakage, but Table needs to handle it.
88
+ content = (
89
+ <FieldSchemaContext.Provider value={fieldChildren || []}>
90
+ <InputSchemaContext.Provider value={inputChildren || []}>
91
+ {content}
92
+ </InputSchemaContext.Provider>
93
+ </FieldSchemaContext.Provider>
94
+ );
95
+ } else {
96
+ // Resource is the same (or not provided), only wrap if we have NEW schema content
97
+ if (fieldChildren) {
98
+ content = (
99
+ <FieldSchemaContext.Provider value={fieldChildren}>
100
+ {content}
101
+ </FieldSchemaContext.Provider>
102
+ );
103
+ }
104
+ if (inputChildren) {
105
+ content = (
106
+ <InputSchemaContext.Provider value={inputChildren}>
107
+ {content}
108
+ </InputSchemaContext.Provider>
109
+ );
110
+ }
111
+ }
112
+
113
+ return <>{content}</>;
114
+ };
@@ -0,0 +1,190 @@
1
+ import React, { createContext, useContext, ReactNode, useState, useCallback, useMemo, ComponentType, Fragment } from 'react';
2
+ import { required } from '@strato-admin/ra-core';
3
+ import { FieldSchema } from './FieldSchema';
4
+ import { InputSchema } from './InputSchema';
5
+
6
+ export interface ResourceSchemas {
7
+ fieldSchema?: ReactNode;
8
+ inputSchema?: ReactNode;
9
+ }
10
+
11
+ export interface DefaultResourceComponents {
12
+ list?: ComponentType<any>;
13
+ create?: ComponentType<any>;
14
+ edit?: ComponentType<any>;
15
+ show?: ComponentType<any>;
16
+ }
17
+
18
+ export interface SchemaRegistryContextValue {
19
+ registerSchemas: (resource: string, schemas: ResourceSchemas) => void;
20
+ getSchemas: (resource: string) => ResourceSchemas | undefined;
21
+ defaultComponents: DefaultResourceComponents;
22
+ }
23
+
24
+ const SchemaRegistryContext = createContext<SchemaRegistryContextValue | undefined>(undefined);
25
+
26
+ // Use a global store for schemas to ensure they are available even before
27
+ // components have finished their first render/useEffect cycle.
28
+ const globalSchemaRegistry: Record<string, ResourceSchemas> = {};
29
+ let globalDefaultComponents: DefaultResourceComponents = {};
30
+ const globalFieldInputMapping = new Map<any, any>();
31
+
32
+ /**
33
+ * Registers default input components for given field components.
34
+ * This mapping is used by ResourceSchemaProvider to infer forms.
35
+ */
36
+ export const registerFieldInputMapping = (mapping: Map<any, any>) => {
37
+ mapping.forEach((value, key) => {
38
+ globalFieldInputMapping.set(key, value);
39
+ });
40
+ };
41
+
42
+ /**
43
+ * Retrieves the registered default input component for a field.
44
+ */
45
+ export const getDefaultInputForField = (fieldComponent: any) => {
46
+ return globalFieldInputMapping.get(fieldComponent);
47
+ };
48
+
49
+ /**
50
+ * Synchronously registers schemas for a resource.
51
+ * Can be called during render or outside of React.
52
+ */
53
+ export const registerGlobalSchemas = (resource: string, schemas: ResourceSchemas) => {
54
+ if (!resource) return;
55
+
56
+ const existing = globalSchemaRegistry[resource];
57
+
58
+ // Only update if we actually have new schema content to avoid redundant re-renders
59
+ if (schemas.fieldSchema || schemas.inputSchema) {
60
+ globalSchemaRegistry[resource] = {
61
+ ...existing,
62
+ fieldSchema: schemas.fieldSchema || existing?.fieldSchema,
63
+ inputSchema: schemas.inputSchema || existing?.inputSchema,
64
+ };
65
+ return true;
66
+ }
67
+ return false;
68
+ };
69
+
70
+ /**
71
+ * Parses unified schema children (Field components) into separate
72
+ * fieldSchema and inputSchema arrays.
73
+ */
74
+ export const parseUnifiedSchema = (children: React.ReactNode): ResourceSchemas => {
75
+ const fieldSchema: React.ReactNode[] = [];
76
+ const inputSchema: React.ReactNode[] = [];
77
+
78
+ const mergeValidation = (isRequired: boolean, validate: any) => {
79
+ if (!isRequired) return validate;
80
+ const requiredValidator = required();
81
+ if (!validate) return requiredValidator;
82
+ if (Array.isArray(validate)) {
83
+ return validate.some((v) => (v as any).isRequired) ? validate : [requiredValidator, ...validate];
84
+ }
85
+ return (validate as any).isRequired ? validate : [requiredValidator, validate];
86
+ };
87
+
88
+ const walk = (nodes: React.ReactNode) => {
89
+ React.Children.forEach(nodes, (child) => {
90
+ if (!React.isValidElement(child)) return;
91
+
92
+ // Handle Fragments and FieldSchema by recursing into their children
93
+ if (child.type === Fragment || child.type === (FieldSchema as any)) {
94
+ walk((child.props as any).children);
95
+ return;
96
+ }
97
+
98
+ // Handle explicit InputSchema by only adding to inputSchema
99
+ if (child.type === (InputSchema as any)) {
100
+ React.Children.forEach((child.props as any).children, (inputChild) => {
101
+ if (React.isValidElement(inputChild)) {
102
+ inputSchema.push(inputChild);
103
+ }
104
+ });
105
+ return;
106
+ }
107
+
108
+ // The field component itself goes into the field schema
109
+ fieldSchema.push(child);
110
+
111
+ // Extract input configuration
112
+ const { source, input, isRequired, description, constraintText, ...rest } = child.props as any;
113
+
114
+ if (input === false) {
115
+ return; // Explicitly excluded from inputs
116
+ }
117
+
118
+ if (React.isValidElement(input)) {
119
+ // Escape hatch: explicit input component provided
120
+ const validate = mergeValidation(isRequired, (input.props as any).validate);
121
+ inputSchema.push(React.cloneElement(input, { ...rest, source, isRequired, validate, description, constraintText }));
122
+ return;
123
+ }
124
+
125
+ // Infer default input component
126
+ const InputComponent = getDefaultInputForField(child.type);
127
+ if (InputComponent) {
128
+ const inputProps = input || {};
129
+ const validate = mergeValidation(isRequired, inputProps.validate);
130
+ inputSchema.push(<InputComponent key={source} {...rest} {...inputProps} source={source} isRequired={isRequired} validate={validate} description={description} constraintText={constraintText} />);
131
+ }
132
+ });
133
+ };
134
+
135
+ walk(children);
136
+
137
+ return {
138
+ fieldSchema: fieldSchema.length > 0 ? fieldSchema : undefined,
139
+ inputSchema: inputSchema.length > 0 ? inputSchema : undefined
140
+ };
141
+ };
142
+
143
+ export const getGlobalSchemas = (resource: string) => globalSchemaRegistry[resource];
144
+
145
+ /**
146
+ * Registers default components to be used by ResourceSchema when not explicitly provided.
147
+ */
148
+ export const registerDefaultResourceComponents = (components: DefaultResourceComponents) => {
149
+ globalDefaultComponents = { ...globalDefaultComponents, ...components };
150
+ };
151
+
152
+ export const getDefaultResourceComponents = () => globalDefaultComponents;
153
+
154
+ export const SchemaRegistryProvider = ({ children }: { children: ReactNode }) => {
155
+ // We still use state to trigger re-renders when new schemas are registered dynamically
156
+ const [, setTick] = useState(0);
157
+
158
+ const registerSchemas = useCallback((resource: string, schemas: ResourceSchemas) => {
159
+ const updated = registerGlobalSchemas(resource, schemas);
160
+ if (updated) {
161
+ setTick((t) => t + 1);
162
+ }
163
+ }, []);
164
+
165
+ const getSchemas = useCallback((resource: string) => globalSchemaRegistry[resource], []);
166
+
167
+ const value = useMemo(() => ({
168
+ registerSchemas,
169
+ getSchemas,
170
+ defaultComponents: globalDefaultComponents
171
+ }), [registerSchemas, getSchemas]);
172
+
173
+ return (
174
+ <SchemaRegistryContext.Provider value={value}>
175
+ {children}
176
+ </SchemaRegistryContext.Provider>
177
+ );
178
+ };
179
+
180
+ export const useSchemaRegistry = () => {
181
+ const context = useContext(SchemaRegistryContext);
182
+ if (!context) {
183
+ return {
184
+ registerSchemas: registerGlobalSchemas,
185
+ getSchemas: getGlobalSchemas,
186
+ defaultComponents: globalDefaultComponents,
187
+ };
188
+ }
189
+ return context;
190
+ };
@@ -0,0 +1,6 @@
1
+ export * from './FieldSchema';
2
+ export * from './InputSchema';
3
+ export * from './ResourceSchema';
4
+ export * from './ResourceSchemaProvider';
5
+ export * from './SchemaRegistry';
6
+ export * from './ResourceContext';