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