edisonywh / backoffice Goto Github PK
View Code? Open in Web Editor NEWAdmin tool built with the PETAL stack
License: MIT License
Admin tool built with the PETAL stack
License: MIT License
Rendering the LiveComponent isn't the hard part, but the hard part is in figuring out how to handle_events
.
The markup would look like this:
Index.Live > FormComponent > CustomField
But users don't really get access to the FormComponent itself, so maybe we can define a generic fallback handle_event
for FormComponent like so (pseudo code):
def handle_event({:update, field, value}) do
# somehow update the current changeset and validate it (and re-render so the child component gets new values/validation error)
end
This means that maybe we can have a bit more of a complex logic in our CustomField component, then just send over a normalized version that can be popped into the changeset.
For example, I have a Listing
form that can belong to a single Newsletter
. I would love to be able to have a typeahead & suggestion box for Newsletters while I'm typing, and then when I click on the value, I want it to store it to the newsletter_id
field in the Listing
changeset instead.
Not sure how feasible this is
EDIT: It doesn't really seem possible because phx-change
works on a form level, and we can't nest form
in HTML (correct me if I'm wrong).
One way to do this is maybe via JS hooks?
Right now Backoffice expects Resolvers to return a page struct, which is basically whatever Scrivener.Ecto returns. However this dependency isn't exactly clear nor is it required/documented. We should either document it, or even remove the dependency on Scrivener.Ecto
and just handle it within Backoffice.
Reference: https://github.com/drewolson/scrivener_ecto/blob/master/lib/scrivener/paginater/ecto/query.ex
zachdaniel from Ash pointed out one feature he has in ash_admin is the ability to have multiple forms. This could be a pretty cool idea, I can imagine the declaration to read something like:
form :create do
...
end
form :edit do
...
end
form :custom do
...
end
Haven't tested it but I don't think this is possible right now. We allow users to pass in their own app.js
, but then Backoffice initiates its own LiveSocket, so I'm not sure how to pass in user-created hooks into the LiveSocket instance
Hi,
I am using MSSQL for my project and whenever I use the index action, it throws the following error
ORDER BY is mandatory when OFFSET is set in query:
from m0 in MyApp.MemberUser,
limit: ^...,
offset: ^...,
select: m0,
preload: [[]]
This is caused by MSSQL's requirement that an ordering column be provided. I have solved this by adding |> order_by(:id)
to the entries
function in your Ecto Resolver.
defp entries(query, repo, page_number, _, page_size) do
offset = page_size * (page_number - 1)
query
|> offset(^offset)
|> limit(^page_size)
|> all(repo)
end
becomes
defp entries(query, repo, page_number, _, page_size) do
offset = page_size * (page_number - 1)
query
|> offset(^offset)
|> limit(^page_size)
|> order_by(:id)
|> all(repo)
end
Would it possible to make this the default behaviour to support MSSQL or to provide a way of defining the order in the use Backoffice.Resource.Index
options
I disabled purging for Tailwind, because there was some issue that's preventing the form fields to render correctly.
Also, if we disable it then it means that custom rendering from userland won't have access to Tailwind (or at least only a subset that's not purged yet).
Backoffice uses the backoffice.html
layout file, but then that only references Backoffice's app.css
, not css files from userland, so this means that we might need to provide a way for user to supply their custom css & js files, and then we can proceed to purge unused Tailwind classes.
The idea is probably for user to supply it in the layout page, just as they do with logo & links now.
Right now we rely on Ecto.Changeset
for forms, we can look into it further and see if we need to do that, or if there's some possibility to move away from Ecto.Changset and implement Phoenix.HTML.FormData ourselves
A few days ago, when we fixed purging I noticed a huge increase in the page load speeds. Turns out we had been sending 3mb in each request had a huge effect - even on localhost. Once we trimmed that to 22KB, the page loads became blazing fast (relatively at least).
As a result, I have been thinking about how else we could optimize Backoffice page loads.
Looking at Kaffy's app.html.eex it seems like they are referencing static files instead of injecting them as raw text into the base layout. This would allow caching of the css and js on the browser.
I think this would work perfectly in our case. Taking the example of one of my pages, the page loads at 193KB. Interestingly, only 42KB is the html. The other 151KB is static css and js. Reducing page sizes by nearly 80% in some cases seems worth pursuing.
Hi,
I've been using backoffice to edit text in a blog-like application. In most cases, we expect multiple sentences and perhaps multiple paragraphs.
To make editing easy, I have been using the :textarea
type on my form field. This however produces json encoded strings.
Not pretty or easy to use.
This seems to be caused by the Phoenix.json_library().encode
applied to the text here
Removing the json encoding seems to resolve the issue
I'm not exactly sure how it'd apply but it's worth investigating
Right now we only show at most one notification, would be nice to be able to stack notifications
Hi @edisonywh,
Are there any plans implement a datetime picker for Ecto date and time types (:utc_datetime
, :naive_datetime
, :date
, :time
, :any
, :utc_datetime_usec
, :naive_datetime_usec
, :time_usec
)?
According to Mozilla's browser documentation, text=[datetime-local]
is not fully supported.. This makes me think we would need a cross browser (read custom) implementation for a consistent experience.
What do you think of using an existing tailwind implementation? Here are some I found:
https://tailwindcomponents.com/component/datepicker-with-tailwindcss-and-alpinejs (incomplete can't change years)
https://tailwindcomponents.com/component/invoice-generator-build-with-tailwindcss-and-alpinejs (has no timepicker)
https://www.vue-tailwind.com/docs/datepicker/ - this is a vue implementation but represents an ideal case. (Disable inline and enable timepicker for the best results (stuck in vueland)
I think we can ignore the lack of time pickers as most people will have no trouble typing them in (default to the current time?) but will benefit from not having to type in dates using the ISO format.
Things become tricky when we have to handle UTC vs local timezone so maybe placing a dropdown with UTC vs local might be of value.
What do you think?
Would be nice to support a more complex filtering system with a nice UI. For example, given an index/1
that looks like:
index do
field :name, :string, search: true
field :age, :integer, search: true
field :verified, :boolean, search: true
end
We know the type of the field, so we should be able to render the corresponding form fields for them (text_input for :string, number_input for :age, checkbox for :boolean). Then on field submission we can package it into a nice query params and have UserLive.Index (Backoffice.Resources) handle the query params, before passing it off to Resolvers.
e.g:
`?name=search&age=>18&verified=true&page=1" then perhaps convert it to something more structured for resolvers to work with?
Then the resolvers receives them in page_otps
and can decide what to do with these fields.
Right now users configure pages with just functions (as callbacks to behaviour), but do we want to continue to do that, or go with a DSL approach that looks similar to Ecto? I generally prefer to keep things as functions and stray away from DSL as much as possible, but I can see a couple of benefits here. I'd love to hear arguments for both side.
With DSL, we:
id: nil
just to display data. I find it pretty strange to pass in nil
just to display something.def MyWeb.UserLive.Index do
use Backoffice.Resources, ...
index do
field :id, :string
field :age, :integer, label: "User Age"
end
form do
field :id, :string
field :age, :integer, label: "User Age"
field :what, :non_existent_type
belongs_to :team, Team
end
end
We map types into form fields, so if we don't know how to render :non_existent_type
, we can error out during compilation.
With DSL we can also maybe simplify the set-up, so we don't need to repeat things like "search_fields/1", and instead:
index do
field :id, :string # defaults to searchable
field :age, :string, search: true,
field :private, :string, search: false
field :verified, :boolean, filter: true
end
You can define everything in one DSL itself.
The filter
field, along with the type
field, most likely doesn't matter in index
, but only in form
, but when used in index
, it can help us build out a more complex FilterComponent (multiple filters, and we know what fields to render.
def index do
[
custom: %{label: "Custom", value: fn resource -> ... end}
]
end
But if we go DSL which does it on compile time, how can we allow user to customize the field with a function? Can we capture a function during compile time, or can we do via MFA?
This might result in a different API, but right now Backoffice does not use your context functions, but rather changesets directly. It is possible for you to use different changesets for different forms, but then this tightly couples to your changesets, and also would it be a better idea to allow users to pipe things into their context functions instead?
Not sure if it's possible since we need some sort of state for table (sorted etc), but we can give it a try and see how it goes.
It might be pretty cool to build everything as a widget, and then by having a TableWidget we can also reuse it when editing associations etc.
Surface seems pretty interesting, I wouldn't mind trying it out but I'm not sure if I want to include another dependency for Backoffice. There are also some things we need to figure out:
I'd love to hear more pros & cons if you have any!
Looking at pull request #41, I expect the UI below.
However, I am instead getting:
The padding and alignments are off. @edisonywh, are you providing some styling locally that are not in the library?
Some more widgets would be good:
With Tailwind and CSS properties we can easily theme the app.
See video: https://youtu.be/MAtaT8BZEAo
The idea is for Backoffice to ship a light and dark theme, and then user can also supply their own theme. Ideally all they need to do is define css properties and then include the CSS files
At the moment there is Backoffice has no clear home/dashboard page. I think this could be a nice value addition. Just a simple page that has the app logo, some widgets with useful stats and some shortcuts to existing resources. @edisonywh, what do you think? Is this a stretch for the library, something outside scope?
What I have in mind is something that looks like:
At a high-level, we would add a new page type. Maybe Backoffice.Page.Dashboard
?
That page would have two functions: widgets
and shortcuts
.
widgets
would return a list of widgets, just like those at in the Index
page.
def widgets(socket) do
%Backoffice.PlainWidget{
title: "Blog Posts",
data: ...
},
%Backoffice.PlainWidget{
title: "Podcast Episodes",
data: ...
},
%Backoffice.PlainWidget{
title: "Unique Visitors",
data: ...
}
end
shortcuts
would return a list of shortcuts to other pages.
def shortcuts do
[
%Backoffice.ShortcutWidget{
title: "Blog",
subtitle: "Get writing",
color: "#23A342",
icon: #svg_data
}
]
Do you think it would be useful to allow links to the new resource page to accept parameters that could allow loading of values into the changeset?
For example, lets say I was dealing with university courses and wanted to add a new class/unit to an existing course.
The ideal experience would be to go to the course I want to change and click on an Add class
.
This would load the url /class/new?course_id=1
and open the create form with the course already provided.
We have Tailwind & Alpine so we should utilize them to have better animations (animating side menus, animating form coming into view, notifications popping up etc).
We should also handle flash messages and display them on the top right corner or something
I came across an issue where I include my app's app.js in Backoffice, which sets up LiveSocket, but the problem is Backoffice already sets up an instance of LiveSocket. I didn't dive too much into what the implications are but from what I saw it was causing handle_event being fired twice.
We need to further investigate this and document it, perhaps suggesting a workaround.
This also tie into how we allow users to supply user defined Hooks to our Backoffice LiveSocket
I've been searching "internal admin tool/dashboard" libraries like this and Kaffy, but I noticed that the last commit to this project was over two years ago. Is this project still being worked on?
I acknowledge this is an open-source project and there is zero obligation for anyone to keep it up-to-date. I was just curious. Thanks!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.