Skip to content

Commit c8b3ec9

Browse files
authored
Add web (wiringbits#3)
* Implement react-admin web * Minor change
1 parent 40b623b commit c8b3ec9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+669
-1
lines changed

Diff for: build.sbt

+39-1
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,51 @@ lazy val spraPlayServer = (project in file("spra-play-server"))
186186
)
187187
)
188188

189+
lazy val bundlerSettings: Project => Project =
190+
_.settings(
191+
Compile / fastOptJS / webpackExtraArgs += "--mode=development",
192+
Compile / fullOptJS / webpackExtraArgs += "--mode=production",
193+
Compile / fastOptJS / webpackDevServerExtraArgs += "--mode=development",
194+
Compile / fullOptJS / webpackDevServerExtraArgs += "--mode=production"
195+
)
196+
197+
lazy val spraWeb = (project in file("spra-web"))
198+
.dependsOn(spraApi.js)
199+
.configure(bundlerSettings, baseLibSettings)
200+
.configure(_.enablePlugins(ScalaJSPlugin, ScalaJSBundlerPlugin))
201+
.settings(
202+
scalaVersion := "2.13.8",
203+
crossScalaVersions := Seq("2.13.8", "3.1.2"),
204+
name := "spra-web",
205+
Test / fork := false, // sjs needs this to run tests
206+
libraryDependencies ++= Seq(
207+
"org.scala-js" %%% "scala-js-macrotask-executor" % "1.0.0",
208+
"me.shadaj" %%% "slinky-core" % "0.7.3",
209+
"me.shadaj" %%% "slinky-web" % "0.7.3"
210+
),
211+
Compile / npmDependencies ++= Seq(
212+
"react" -> "17.0.0",
213+
"react-dom" -> "17.0.0",
214+
"react-scripts" -> "5.0.0",
215+
"react-admin" -> "4.1.0",
216+
"ra-ui-materialui" -> "4.1.0",
217+
"ra-data-simple-rest" -> "4.1.0",
218+
"ra-i18n-polyglot" -> "4.1.0",
219+
"ra-language-english" -> "4.1.0",
220+
"ra-core" -> "4.1.0",
221+
"@mui/material" -> "5.8.1",
222+
"@emotion/styled" -> "11.8.1"
223+
)
224+
)
225+
189226
lazy val root = (project in file("."))
190227
.aggregate(
191228
spraCommon.jvm,
192229
spraCommon.js,
193230
spraApi.jvm,
194231
spraApi.js,
195-
spraPlayServer
232+
spraPlayServer,
233+
spraWeb
196234
)
197235
.settings(
198236
name := "scala-postgres-react-admin",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package net.wiringbits.webapp.utils.ui.web
2+
3+
import net.wiringbits.webapp.utils.api.AdminDataExplorerApiClient
4+
5+
case class API(client: AdminDataExplorerApiClient, url: String)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package net.wiringbits.webapp.utils.ui.web
2+
3+
import net.wiringbits.webapp.utils.api.models.AdminGetTables
4+
import net.wiringbits.webapp.utils.ui.web.components.{EditGuesser, ListGuesser}
5+
import net.wiringbits.webapp.utils.ui.web.facades.reactadmin._
6+
import net.wiringbits.webapp.utils.ui.web.facades.simpleRestProvider
7+
import net.wiringbits.webapp.utils.ui.web.models.DataExplorerSettings
8+
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global
9+
import slinky.core.facade.{Hooks, ReactElement}
10+
import slinky.core.{FunctionalComponent, KeyAddingStage}
11+
import slinky.web.html.{div, h1}
12+
13+
import scala.util.{Failure, Success}
14+
15+
object AdminView {
16+
case class Props(api: API, dataExplorerSettings: DataExplorerSettings = DataExplorerSettings())
17+
18+
def apply(api: API, dataExplorerSettings: DataExplorerSettings = DataExplorerSettings()): KeyAddingStage = component(
19+
Props(api, dataExplorerSettings)
20+
)
21+
22+
val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>
23+
val (tables, setTables) = Hooks.useState[List[AdminGetTables.Response.DatabaseTable]](List.empty)
24+
val (error, setError) = Hooks.useState[Option[String]](Option.empty)
25+
26+
// We have to find a way to use AsyncComponent instead of useEffect
27+
Hooks.useEffect(
28+
() => {
29+
props.api.client.getTables.onComplete {
30+
case Success(response) =>
31+
setTables(response.data)
32+
setError(None)
33+
34+
case Failure(ex) =>
35+
setError(Some(ex.getMessage))
36+
}
37+
},
38+
""
39+
)
40+
41+
val tablesUrl = s"${props.api.url}/admin/tables"
42+
43+
def buildResources: Seq[ReactElement] = {
44+
tables.map { table =>
45+
Resource(
46+
Resource.Props(
47+
name = table.name,
48+
list = ListGuesser(table),
49+
edit = EditGuesser(table, props.dataExplorerSettings)
50+
)
51+
)
52+
}
53+
}
54+
55+
div()(
56+
Admin(Admin.Props(dataProvider = simpleRestProvider(tablesUrl), children = buildResources)),
57+
error.map(h1(_))
58+
)
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package net.wiringbits.webapp.utils.ui.web.components
2+
3+
import net.wiringbits.webapp.utils.api.models.AdminGetTables
4+
import net.wiringbits.webapp.utils.ui.web.facades.reactadmin.ReactAdmin.useEditContext
5+
import net.wiringbits.webapp.utils.ui.web.facades.reactadmin._
6+
import net.wiringbits.webapp.utils.ui.web.models.{ButtonAction, ColumnType, DataExplorerSettings}
7+
import net.wiringbits.webapp.utils.ui.web.utils.ResponseGuesser
8+
import org.scalajs.dom
9+
import org.scalajs.macrotaskexecutor.MacrotaskExecutor.Implicits.global
10+
import slinky.core.facade.{Fragment, ReactElement}
11+
import slinky.core.{FunctionalComponent, KeyAddingStage}
12+
13+
import scala.scalajs.js
14+
import scala.util.{Failure, Success}
15+
16+
object EditGuesser {
17+
case class Props(response: AdminGetTables.Response.DatabaseTable, dataExplorerSettings: DataExplorerSettings)
18+
19+
def apply(
20+
response: AdminGetTables.Response.DatabaseTable,
21+
dataExplorerSettings: DataExplorerSettings
22+
): KeyAddingStage = {
23+
component(Props(response, dataExplorerSettings))
24+
}
25+
26+
val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>
27+
val fields = ResponseGuesser.getTypesFromResponse(props.response)
28+
val inputs: Seq[ReactElement] = fields.map { field =>
29+
field.`type` match {
30+
case ColumnType.Date => DateTimeInput(DateTimeInput.Props(source = field.name, disabled = field.disabled))
31+
case ColumnType.Text => TextInput(TextInput.Props(source = field.name, disabled = field.disabled))
32+
case ColumnType.Email => TextInput(TextInput.Props(source = field.name, disabled = field.disabled))
33+
case ColumnType.Image => ImageField(ImageField.Props(source = field.name))
34+
case ColumnType.Number => NumberInput(NumberInput.Props(source = field.name, disabled = field.disabled))
35+
case ColumnType.Reference(reference, source) =>
36+
ReferenceInput(
37+
ReferenceInput.Props(
38+
source = field.name,
39+
reference = reference,
40+
children = Seq(SelectInput(SelectInput.Props(optionText = source, disabled = field.disabled)))
41+
)
42+
)
43+
}
44+
}
45+
46+
def onClick(action: ButtonAction, ctx: js.Dictionary[js.Any]): Unit = {
47+
val primaryKey = dom.window.location.hash.split("/").lastOption.getOrElse("")
48+
action.onClick(primaryKey).onComplete {
49+
case Failure(ex) => ex.printStackTrace()
50+
case Success(_) => refetch(ctx)
51+
}
52+
}
53+
54+
def refetch(ctx: js.Dictionary[js.Any]): Unit = {
55+
val _ = ctx.get("refetch").map(_.asInstanceOf[js.Dynamic].apply())
56+
}
57+
58+
val tableAction = props.dataExplorerSettings.actions.find(_.tableName == props.response.name)
59+
60+
def buttons(): Seq[ReactElement] = {
61+
val ctx = useEditContext()
62+
tableAction
63+
.map { x =>
64+
x.actions.map { action =>
65+
Button(Button.Props(onClick = () => onClick(action, ctx), children = Seq(action.text)))
66+
}: Seq[ReactElement]
67+
}
68+
.getOrElse(Seq.empty)
69+
}
70+
71+
val actions = TopToolbar(TopToolbar.Props(children = buttons()))
72+
73+
val deleteButton: ReactElement = if (props.response.canBeDeleted) DeleteButton() else Fragment()
74+
val toolbar: ReactElement = Toolbar(
75+
Toolbar.Props(children =
76+
Seq(
77+
SaveButton(),
78+
deleteButton
79+
)
80+
)
81+
)
82+
83+
Edit(
84+
Edit.Props(
85+
actions = actions(),
86+
children = Seq(SimpleForm(SimpleForm.Props(toolbar = toolbar, children = inputs)))
87+
)
88+
)
89+
}
90+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package net.wiringbits.webapp.utils.ui.web.components
2+
3+
import net.wiringbits.webapp.utils.api.models.AdminGetTables
4+
import net.wiringbits.webapp.utils.ui.web.facades.reactadmin._
5+
import net.wiringbits.webapp.utils.ui.web.models.ColumnType
6+
import net.wiringbits.webapp.utils.ui.web.utils.ResponseGuesser
7+
import slinky.core.facade.{Fragment, ReactElement}
8+
import slinky.core.{FunctionalComponent, KeyAddingStage}
9+
10+
import scala.scalajs.js
11+
12+
object ListGuesser {
13+
case class Props(response: AdminGetTables.Response.DatabaseTable)
14+
15+
def apply(response: AdminGetTables.Response.DatabaseTable): KeyAddingStage = {
16+
component(Props(response))
17+
}
18+
19+
val component: FunctionalComponent[Props] = FunctionalComponent[Props] { props =>
20+
val fields = ResponseGuesser.getTypesFromResponse(props.response)
21+
22+
def defaultField(reference: String, source: String, children: Seq[ReactElement]): ReactElement =
23+
ReferenceField(
24+
ReferenceField.Props(
25+
reference = reference,
26+
source = source,
27+
children = children
28+
)
29+
)
30+
31+
val widgetFields: Seq[ReactElement] = fields.map { field =>
32+
val imageStyles = js.Dynamic.literal("width" -> "100px")
33+
val styles = js.Dynamic.literal("& img" -> imageStyles)
34+
field.`type` match {
35+
case ColumnType.Date => DateField(DateField.Props(source = field.name, showTime = true))
36+
case ColumnType.Text => TextField(TextField.Props(source = field.name))
37+
case ColumnType.Email => EmailField(EmailField.Props(source = field.name))
38+
case ColumnType.Image => ImageField(ImageField.Props(source = field.name, sx = styles))
39+
case ColumnType.Number => NumberField(NumberField.Props(source = field.name))
40+
case ColumnType.Reference(reference, source) =>
41+
defaultField(reference, field.name, Seq(TextField(TextField.Props(source = source))))
42+
}
43+
}
44+
45+
val filterList: Seq[ReactElement] = fields.filter(_.filterable).map { field =>
46+
field.`type` match {
47+
case ColumnType.Date => DateInput(DateInput.Props(source = field.name))
48+
case ColumnType.Text | ColumnType.Email => TextInput(TextInput.Props(source = field.name))
49+
case ColumnType.Image => Fragment()
50+
case ColumnType.Number => NumberInput(NumberInput.Props(source = field.name))
51+
case ColumnType.Reference(reference, source) =>
52+
defaultField(reference, field.name, Seq(TextField(TextField.Props(source = source))))
53+
}
54+
}
55+
56+
val listToolbar: ReactElement = TopToolbar(
57+
TopToolbar.Props(
58+
children = Seq(
59+
FilterButton(FilterButton.Props(filters = filterList)),
60+
ExportButton()
61+
)
62+
)
63+
)
64+
65+
ComponentList(ComponentList.Props(actions = listToolbar, filters = filterList))(
66+
Datagrid(
67+
Datagrid.Props(rowClick = "edit", bulkActionButtons = props.response.canBeDeleted, children = widgetFields)
68+
)
69+
)
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package net.wiringbits.webapp.utils.ui.web.facades
2+
3+
import scala.scalajs.js
4+
5+
@js.native
6+
trait DataProvider extends js.Object
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package net.wiringbits.webapp.utils.ui.web
2+
3+
import scala.scalajs.js
4+
import scala.scalajs.js.annotation.JSImport
5+
6+
package object facades {
7+
@js.native
8+
@JSImport("ra-data-simple-rest", JSImport.Default)
9+
def simpleRestProvider(url: String): DataProvider = js.native
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package net.wiringbits.webapp.utils.ui.web.facades.reactadmin
2+
3+
import net.wiringbits.webapp.utils.ui.web.facades.DataProvider
4+
import slinky.core.ExternalComponent
5+
import slinky.core.facade.ReactElement
6+
7+
import scala.scalajs.js
8+
import scala.scalajs.js.|
9+
10+
object Admin extends ExternalComponent {
11+
case class Props(dataProvider: DataProvider, children: Seq[ReactElement])
12+
override val component: String | js.Object = ReactAdmin.Admin
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package net.wiringbits.webapp.utils.ui.web.facades.reactadmin
2+
3+
import slinky.core.ExternalComponent
4+
import slinky.core.facade.ReactElement
5+
6+
import scala.scalajs.js
7+
import scala.scalajs.js.|
8+
9+
object Button extends ExternalComponent {
10+
case class Props(onClick: () => Unit, children: Seq[ReactElement])
11+
override val component: String | js.Object = ReactAdmin.Button
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package net.wiringbits.webapp.utils.ui.web.facades.reactadmin
2+
3+
import slinky.core.ExternalComponent
4+
import slinky.core.facade.ReactElement
5+
6+
import scala.scalajs.js
7+
import scala.scalajs.js.|
8+
9+
object ComponentList extends ExternalComponent {
10+
case class Props(actions: ReactElement, filters: Seq[ReactElement])
11+
override val component: String | js.Object = ReactAdmin.List
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package net.wiringbits.webapp.utils.ui.web.facades.reactadmin
2+
3+
import slinky.core.ExternalComponent
4+
import slinky.core.facade.ReactElement
5+
6+
import scala.scalajs.js
7+
import scala.scalajs.js.|
8+
9+
object Datagrid extends ExternalComponent {
10+
case class Props(rowClick: String, bulkActionButtons: Boolean, children: Seq[ReactElement])
11+
override val component: String | js.Object = ReactAdmin.Datagrid
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package net.wiringbits.webapp.utils.ui.web.facades.reactadmin
2+
3+
import slinky.core.ExternalComponent
4+
5+
import scala.scalajs.js
6+
import scala.scalajs.js.|
7+
8+
object DateField extends ExternalComponent {
9+
case class Props(source: String, showTime: Boolean)
10+
override val component: String | js.Object = ReactAdmin.DateField
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package net.wiringbits.webapp.utils.ui.web.facades.reactadmin
2+
3+
import slinky.core.ExternalComponent
4+
5+
import scala.scalajs.js
6+
import scala.scalajs.js.|
7+
8+
object DateInput extends ExternalComponent {
9+
case class Props(source: String)
10+
override val component: String | js.Object = ReactAdmin.DateInput
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package net.wiringbits.webapp.utils.ui.web.facades.reactadmin
2+
3+
import slinky.core.ExternalComponent
4+
5+
import scala.scalajs.js
6+
import scala.scalajs.js.|
7+
8+
object DateTimeInput extends ExternalComponent {
9+
case class Props(source: String, disabled: Boolean = false)
10+
override val component: String | js.Object = ReactAdmin.DateTimeInput
11+
}

0 commit comments

Comments
 (0)