diff --git a/package-lock.json b/package-lock.json
index a671df5..c10ca6b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7,6 +7,7 @@
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-aspect-ratio": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
@@ -3682,6 +3683,29 @@
}
}
},
+ "node_modules/@radix-ui/react-aspect-ratio": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.0.3.tgz",
+ "integrity": "sha512-fXR5kbMan9oQqMuacfzlGG/SQMcmMlZ4wrvpckv8SgUulD0MMpspxJrxg/Gp/ISV3JfV1AeSWTYK9GvxA4ySwA==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-primitive": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-avatar": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz",
diff --git a/package.json b/package.json
index 4652448..c61fc07 100644
--- a/package.json
+++ b/package.json
@@ -7,6 +7,7 @@
"dependencies": {
"@hookform/resolvers": "^3.3.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
+ "@radix-ui/react-aspect-ratio": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-checkbox": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
diff --git a/src/collapsar/File.py b/src/collapsar/File.py
new file mode 100644
index 0000000..222cb5f
--- /dev/null
+++ b/src/collapsar/File.py
@@ -0,0 +1,41 @@
+"""FileField definition"""
+from typing import Callable, Union, TYPE_CHECKING
+from masonite.facades.Hash import Hash
+from .Field import Field
+
+if TYPE_CHECKING:
+ from masoniteorm.models.Model import Model
+
+
+class File(Field):
+ """FileField definition"""
+
+ type = "file"
+
+ def __init__(
+ self, name: str, attribute: Union[str, Callable] = None, resolve_callback: Callable = None
+ ):
+ # Field's component
+ self.component = "FileField"
+ # Field's suggestions callback
+ self.suggestions = None
+
+ super().__init__(name, attribute, resolve_callback)
+
+ def fill(self, request, model: "Model"):
+ """Fill the field"""
+
+ storage = request.app.make("storage")
+ path = storage.disk("local").put_file("collapsar/storage/", request.input(self.attribute))
+ setattr(model, self.attribute, path)
+
+ return None
+
+ def json_serialize(self):
+ """
+ Prepare the element for JSON serialization.
+
+ :return: dict
+ """
+ serialized = super().json_serialize()
+ return serialized
diff --git a/src/collapsar/assets/js/components/fields/FileField.tsx b/src/collapsar/assets/js/components/fields/FileField.tsx
new file mode 100644
index 0000000..325ff53
--- /dev/null
+++ b/src/collapsar/assets/js/components/fields/FileField.tsx
@@ -0,0 +1,41 @@
+import { Input } from "@/components/ui/input";
+import { AspectRatio } from "@radix-ui/react-aspect-ratio";
+import { useState } from "react";
+
+export function FileField(props: any) {
+ const [replaceImage, setReplaceImage] = useState(true);
+
+ const displayRender = () => {
+ return (
+
+
+
+ );
+ };
+
+ if (props.renderForDisplay || !props.fieldConfig) {
+ return displayRender();
+ }
+
+ const handleOnChange = (e: any) => {
+ props.onChange(e.target.files ? e.target.files[0] : null)
+ }
+
+ return (
+ <>
+ {props.value && !replaceImage ? (
+
+ setReplaceImage(true)} />
+
+ ) : (
+
+ )}
+ >
+ );
+}
diff --git a/src/collapsar/assets/js/components/fields/index.js b/src/collapsar/assets/js/components/fields/index.js
index c1a9e29..2cb7081 100644
--- a/src/collapsar/assets/js/components/fields/index.js
+++ b/src/collapsar/assets/js/components/fields/index.js
@@ -4,6 +4,7 @@ export { SelectField } from './SelectField';
export { BooleanField } from './BooleanField';
export { RichTextField } from './RichTextField';
export { CalendarField } from './CalendarField';
+export { FileField } from './FileField';
export { BelongsToField } from './BelongsToField';
export { Field } from './Field';
\ No newline at end of file
diff --git a/src/collapsar/assets/js/components/ui/aspect-ratio.tsx b/src/collapsar/assets/js/components/ui/aspect-ratio.tsx
new file mode 100644
index 0000000..c4abbf3
--- /dev/null
+++ b/src/collapsar/assets/js/components/ui/aspect-ratio.tsx
@@ -0,0 +1,5 @@
+import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio"
+
+const AspectRatio = AspectRatioPrimitive.Root
+
+export { AspectRatio }
diff --git a/src/collapsar/assets/js/pages/ResourceEdit.tsx b/src/collapsar/assets/js/pages/ResourceEdit.tsx
index 608f91c..e1c231b 100644
--- a/src/collapsar/assets/js/pages/ResourceEdit.tsx
+++ b/src/collapsar/assets/js/pages/ResourceEdit.tsx
@@ -59,6 +59,9 @@ export function ResourceEdit() {
let schema: z.ZodTypeAny;
switch (field.type) {
+ case "file":
+ schema = z.instanceof(File);
+ break;
case "boolean":
schema = z.boolean();
break;
@@ -128,16 +131,16 @@ export function ResourceEdit() {
function onSubmit(values: any) {
// filter values object to remove computed fields
- values = Object.keys(values)
+ const formData = new FormData()
+ Object.keys(values)
.filter((key) => !key.startsWith("computed"))
- .reduce((obj, key) => {
- obj[key] = values[key];
- return obj;
- }, {});
+ .forEach(key => {
+ formData.append(key, values[key])
+ });
if (isCreating) {
return axios
- .put(`/collapsar-api/${params.resource}/`, values)
+ .put(`/collapsar-api/${params.resource}/`, formData)
.then((response) => {
console.log("Success");
console.log(response.data);
@@ -149,7 +152,7 @@ export function ResourceEdit() {
}
axios
- .patch(`/collapsar-api/${params.resource}/${data.data.id}`, values)
+ .patch(`/collapsar-api/${params.resource}/${data.data.id}`, formData)
.then((response) => {
console.log("Success");
console.log(response.data);
diff --git a/src/collapsar/controllers/CollapsarController.py b/src/collapsar/controllers/CollapsarController.py
index 5f17a3c..2b3dbe8 100644
--- a/src/collapsar/controllers/CollapsarController.py
+++ b/src/collapsar/controllers/CollapsarController.py
@@ -30,3 +30,9 @@ def get_css(self, response: Response):
def get_asset(self, filename):
"""Return file path."""
return os.path.dirname(__file__) + "/../dist/assets/" + filename
+
+ def get_storage(self, request: Request, response: Response):
+ """Return file path."""
+ storage = request.app.make("storage")
+ file_name = request.environ.get('PATH_INFO').split('/')[-1].replace('..', '')
+ return response.download(file_name, storage.disk('local').get_path('collapsar/storage/' + file_name))
diff --git a/src/collapsar/providers/CollapsarProvider.py b/src/collapsar/providers/CollapsarProvider.py
index c3a6bad..37fff17 100644
--- a/src/collapsar/providers/CollapsarProvider.py
+++ b/src/collapsar/providers/CollapsarProvider.py
@@ -36,6 +36,7 @@ def register(self):
super().register()
+ # TODO: Make this configurable
resources_path = config("collapsar.resources_path", "app/collapsar/resources")
self.application.bind("Collapsar", Collapsar(self.application))
diff --git a/src/collapsar/routes/web.py b/src/collapsar/routes/web.py
index 7559263..80c8c07 100644
--- a/src/collapsar/routes/web.py
+++ b/src/collapsar/routes/web.py
@@ -9,10 +9,13 @@
Route.post("/auth/login", AuthController.login),
Route.get("/auth/logout", AuthController.logout),
+ Route.get("/storage/.*", CollapsarController.get_storage),
Route.get("/assets/app.js", CollapsarController.get_js),
Route.get("/assets/style.css", CollapsarController.get_css),
- Route.get('.*', CollapsarController.index).middleware('auth',)
+
+ # Route.get('', CollapsarController.index).middleware('auth',),
+ Route.get('.*', CollapsarController.index).middleware('auth',),
],
prefix="/collapsar",
)
diff --git a/tests/integrations/app/collapsar/resources/ArticleResource.py b/tests/integrations/app/collapsar/resources/ArticleResource.py
index d7fa8ce..f5e929b 100644
--- a/tests/integrations/app/collapsar/resources/ArticleResource.py
+++ b/tests/integrations/app/collapsar/resources/ArticleResource.py
@@ -1,11 +1,11 @@
from tests.integrations.app.models.Article import Article
-from UserResource import UserResource
from src.collapsar import Resource
from src.collapsar.TextInput import TextInput
from src.collapsar.Id import Id
from src.collapsar.RichText import RichText
from src.collapsar.BelongsTo import BelongsTo
+from src.collapsar.File import File
class ArticleResource(Resource):
@@ -26,6 +26,7 @@ def fields(cls):
return [
Id("Id", "id").readonly(),
+ File("Image", "image"),
TextInput("Title", "title").rules("required"),
RichText("Content", "content").rules("required").hide_from_index(),
BelongsTo("User", "user", "UserResource").rules("required"),
diff --git a/tests/integrations/app/models/Article.py b/tests/integrations/app/models/Article.py
index 1cbdf46..17c3f32 100644
--- a/tests/integrations/app/models/Article.py
+++ b/tests/integrations/app/models/Article.py
@@ -6,7 +6,7 @@
class Article(Model, UUIDPrimaryKeyMixin):
"""Article Model"""
- __fillable__ = ["title", "content", "user_id"]
+ __fillable__ = ["title", "content", "user_id", "image"]
@belongs_to('user_id', 'id')
def user(self):
diff --git a/tests/integrations/databases/migrations/2023_10_26_214109_articles_add_image.py b/tests/integrations/databases/migrations/2023_10_26_214109_articles_add_image.py
new file mode 100644
index 0000000..11a4904
--- /dev/null
+++ b/tests/integrations/databases/migrations/2023_10_26_214109_articles_add_image.py
@@ -0,0 +1,19 @@
+"""ArticlesAddImage Migration."""
+
+from masoniteorm.migrations import Migration
+
+
+class ArticlesAddImage(Migration):
+ def up(self):
+ """
+ Run the migrations.
+ """
+ with self.schema.table("articles") as table:
+ table.string("image").nullable()
+
+ def down(self):
+ """
+ Revert the migrations.
+ """
+ with self.schema.table("articles") as table:
+ table.drop_column("image")
diff --git a/tests/integrations/storage/framework/filesystem/collapsar/storage/27a53723-0ab5-4dd4-af92-b86ba63b2d02.png b/tests/integrations/storage/framework/filesystem/collapsar/storage/27a53723-0ab5-4dd4-af92-b86ba63b2d02.png
new file mode 100644
index 0000000..e48f8e6
Binary files /dev/null and b/tests/integrations/storage/framework/filesystem/collapsar/storage/27a53723-0ab5-4dd4-af92-b86ba63b2d02.png differ
diff --git a/tests/integrations/storage/framework/filesystem/collapsar/storage/f5e64ec6-e148-4f4a-bb4b-8c1bbdf1def9.png b/tests/integrations/storage/framework/filesystem/collapsar/storage/f5e64ec6-e148-4f4a-bb4b-8c1bbdf1def9.png
new file mode 100644
index 0000000..e48f8e6
Binary files /dev/null and b/tests/integrations/storage/framework/filesystem/collapsar/storage/f5e64ec6-e148-4f4a-bb4b-8c1bbdf1def9.png differ