diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala index 4557514..930c815 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala @@ -17,18 +17,9 @@ .amend(svg.cls := "flex-shrink-0 text-gray-400"), span(cls("ml-2 flex-1 w-0 truncate"), m.name) ), - div( - cls("ml-4 flex-shrink-0 flex space-x-4"), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ), - span(cls("text-gray-300"), "|"), - a( - href("#"), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Odebrat" - ) + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala index 4557514..930c815 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala @@ -17,18 +17,9 @@ .amend(svg.cls := "flex-shrink-0 text-gray-400"), span(cls("ml-2 flex-1 w-0 truncate"), m.name) ), - div( - cls("ml-4 flex-shrink-0 flex space-x-4"), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ), - span(cls("text-gray-300"), "|"), - a( - href("#"), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Odebrat" - ) + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala index 554515d..12bbac9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala @@ -2,23 +2,11 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import io.laminext.syntax.core.{*, given} -case class FileList( - files: Signal[List[File]] -) - -object FileList: - extension (m: FileList) - def toHtml: HtmlElement = - div( - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - children <-- m.files.map(_.map(_.toHtml)) - ), - button( - tpe := "button", - cls := "mt-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Přidat soubor" - ) - ) +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala index 4557514..930c815 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala @@ -17,18 +17,9 @@ .amend(svg.cls := "flex-shrink-0 text-gray-400"), span(cls("ml-2 flex-1 w-0 truncate"), m.name) ), - div( - cls("ml-4 flex-shrink-0 flex space-x-4"), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ), - span(cls("text-gray-300"), "|"), - a( - href("#"), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Odebrat" - ) + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala index 554515d..12bbac9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala @@ -2,23 +2,11 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import io.laminext.syntax.core.{*, given} -case class FileList( - files: Signal[List[File]] -) - -object FileList: - extension (m: FileList) - def toHtml: HtmlElement = - div( - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - children <-- m.files.map(_.map(_.toHtml)) - ), - button( - tpe := "button", - cls := "mt-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Přidat soubor" - ) - ) +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala index 6fdbaa2..b4e506a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala @@ -1,13 +1,15 @@ package cz.e_bs.cmi.mdr.pdb.app.components.files import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} object FilePicker: import cz.e_bs.cmi.mdr.pdb.app.components.files - val File = files.File - val Selector = FileSelector _ - def apply(display: Signal[List[File]] => HtmlElement): HtmlElement = + val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] + val selectorOpen = Var[Boolean](false) + + def apply(chosenFiles: Var[List[File]]): HtmlElement = // This sequence tricks browser into displaying modal content centered // Inspired by modal in headless ui playground // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 @@ -29,43 +31,62 @@ */ div( div( + onClick.mapTo(false) --> selectorOpen.writer, cls("fixed inset-0 transition-opacity"), div(cls("absolute inset-0 bg-gray-500 opacity-75")) ) ) - val selectedFiles = Var[Set[File]](Set.empty) - - div( + inline def modalSelector: HtmlElement = div( cls("fixed inset-0 z-10 overflow-y-auto"), div( - cls( - "text-center sm:block sm:p-0" - ), + cls("text-center sm:block sm:p-0"), overlay, browserCenteringModalTrick, - Selector( - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") + child <-- chosenFiles.signal.map( + FileSelector( + _, + Val( + List( + File( + "https://tc163.cmi.cz/first_file", + "Smlouva o pracovním poměru" + ), + File( + "https://tc163.cmi.cz/first_file", + "Vysokoškolský diplom" + ), + File( + "https://tc163.cmi.cz/first_file", + "Prezenční listina školení" + ), + File("https://tc163.cmi.cz/first_file", "Životopis") + ) ) - ), - selectedFiles + )(fsaObserver) ) ) + ) + + div( + fsaStream.collect { case FileSelector.SelectionUpdated(files) => + files.to(List) + } --> chosenFiles.writer, + fsaStream.mapTo(false) --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None ), - display(selectedFiles.signal.map(_.to(List))) + div( + cls("flex flex-col space-y-5"), + child.maybe <-- chosenFiles.signal.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala index 4557514..930c815 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala @@ -17,18 +17,9 @@ .amend(svg.cls := "flex-shrink-0 text-gray-400"), span(cls("ml-2 flex-1 w-0 truncate"), m.name) ), - div( - cls("ml-4 flex-shrink-0 flex space-x-4"), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ), - span(cls("text-gray-300"), "|"), - a( - href("#"), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Odebrat" - ) + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala index 554515d..12bbac9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala @@ -2,23 +2,11 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import io.laminext.syntax.core.{*, given} -case class FileList( - files: Signal[List[File]] -) - -object FileList: - extension (m: FileList) - def toHtml: HtmlElement = - div( - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - children <-- m.files.map(_.map(_.toHtml)) - ), - button( - tpe := "button", - cls := "mt-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Přidat soubor" - ) - ) +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala index 6fdbaa2..b4e506a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala @@ -1,13 +1,15 @@ package cz.e_bs.cmi.mdr.pdb.app.components.files import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} object FilePicker: import cz.e_bs.cmi.mdr.pdb.app.components.files - val File = files.File - val Selector = FileSelector _ - def apply(display: Signal[List[File]] => HtmlElement): HtmlElement = + val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] + val selectorOpen = Var[Boolean](false) + + def apply(chosenFiles: Var[List[File]]): HtmlElement = // This sequence tricks browser into displaying modal content centered // Inspired by modal in headless ui playground // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 @@ -29,43 +31,62 @@ */ div( div( + onClick.mapTo(false) --> selectorOpen.writer, cls("fixed inset-0 transition-opacity"), div(cls("absolute inset-0 bg-gray-500 opacity-75")) ) ) - val selectedFiles = Var[Set[File]](Set.empty) - - div( + inline def modalSelector: HtmlElement = div( cls("fixed inset-0 z-10 overflow-y-auto"), div( - cls( - "text-center sm:block sm:p-0" - ), + cls("text-center sm:block sm:p-0"), overlay, browserCenteringModalTrick, - Selector( - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") + child <-- chosenFiles.signal.map( + FileSelector( + _, + Val( + List( + File( + "https://tc163.cmi.cz/first_file", + "Smlouva o pracovním poměru" + ), + File( + "https://tc163.cmi.cz/first_file", + "Vysokoškolský diplom" + ), + File( + "https://tc163.cmi.cz/first_file", + "Prezenční listina školení" + ), + File("https://tc163.cmi.cz/first_file", "Životopis") + ) ) - ), - selectedFiles + )(fsaObserver) ) ) + ) + + div( + fsaStream.collect { case FileSelector.SelectionUpdated(files) => + files.to(List) + } --> chosenFiles.writer, + fsaStream.mapTo(false) --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None ), - display(selectedFiles.signal.map(_.to(List))) + div( + cls("flex flex-col space-y-5"), + child.maybe <-- chosenFiles.signal.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala index 3822cc1..a14d16d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala @@ -6,138 +6,62 @@ import io.laminext.syntax.core.{*, given} import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), +object FileSelector: + sealed trait Action + case class SelectionUpdated(files: Set[File]) extends Action + case object SelectionCancelled extends Action + def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( + selectionDone: Observer[Action] + ): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) - -def FileSelector( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) ) - ) + ), + FileTable(availableFiles, selectedFiles) ), - FileTable(files, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit" - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit" + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionDone + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionDone + ) ) ) ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala index 4557514..930c815 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala @@ -17,18 +17,9 @@ .amend(svg.cls := "flex-shrink-0 text-gray-400"), span(cls("ml-2 flex-1 w-0 truncate"), m.name) ), - div( - cls("ml-4 flex-shrink-0 flex space-x-4"), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ), - span(cls("text-gray-300"), "|"), - a( - href("#"), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Odebrat" - ) + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala index 554515d..12bbac9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala @@ -2,23 +2,11 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import io.laminext.syntax.core.{*, given} -case class FileList( - files: Signal[List[File]] -) - -object FileList: - extension (m: FileList) - def toHtml: HtmlElement = - div( - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - children <-- m.files.map(_.map(_.toHtml)) - ), - button( - tpe := "button", - cls := "mt-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Přidat soubor" - ) - ) +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala index 6fdbaa2..b4e506a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala @@ -1,13 +1,15 @@ package cz.e_bs.cmi.mdr.pdb.app.components.files import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} object FilePicker: import cz.e_bs.cmi.mdr.pdb.app.components.files - val File = files.File - val Selector = FileSelector _ - def apply(display: Signal[List[File]] => HtmlElement): HtmlElement = + val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] + val selectorOpen = Var[Boolean](false) + + def apply(chosenFiles: Var[List[File]]): HtmlElement = // This sequence tricks browser into displaying modal content centered // Inspired by modal in headless ui playground // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 @@ -29,43 +31,62 @@ */ div( div( + onClick.mapTo(false) --> selectorOpen.writer, cls("fixed inset-0 transition-opacity"), div(cls("absolute inset-0 bg-gray-500 opacity-75")) ) ) - val selectedFiles = Var[Set[File]](Set.empty) - - div( + inline def modalSelector: HtmlElement = div( cls("fixed inset-0 z-10 overflow-y-auto"), div( - cls( - "text-center sm:block sm:p-0" - ), + cls("text-center sm:block sm:p-0"), overlay, browserCenteringModalTrick, - Selector( - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") + child <-- chosenFiles.signal.map( + FileSelector( + _, + Val( + List( + File( + "https://tc163.cmi.cz/first_file", + "Smlouva o pracovním poměru" + ), + File( + "https://tc163.cmi.cz/first_file", + "Vysokoškolský diplom" + ), + File( + "https://tc163.cmi.cz/first_file", + "Prezenční listina školení" + ), + File("https://tc163.cmi.cz/first_file", "Životopis") + ) ) - ), - selectedFiles + )(fsaObserver) ) ) + ) + + div( + fsaStream.collect { case FileSelector.SelectionUpdated(files) => + files.to(List) + } --> chosenFiles.writer, + fsaStream.mapTo(false) --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None ), - display(selectedFiles.signal.map(_.to(List))) + div( + cls("flex flex-col space-y-5"), + child.maybe <-- chosenFiles.signal.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala index 3822cc1..a14d16d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala @@ -6,138 +6,62 @@ import io.laminext.syntax.core.{*, given} import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), +object FileSelector: + sealed trait Action + case class SelectionUpdated(files: Set[File]) extends Action + case object SelectionCancelled extends Action + def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( + selectionDone: Observer[Action] + ): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) - -def FileSelector( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) ) - ) + ), + FileTable(availableFiles, selectedFiles) ), - FileTable(files, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit" - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit" + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionDone + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionDone + ) ) ) ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala new file mode 100644 index 0000000..0fcc458 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala @@ -0,0 +1,90 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.files + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import cz.e_bs.cmi.mdr.pdb.app.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala index 4557514..930c815 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/File.scala @@ -17,18 +17,9 @@ .amend(svg.cls := "flex-shrink-0 text-gray-400"), span(cls("ml-2 flex-1 w-0 truncate"), m.name) ), - div( - cls("ml-4 flex-shrink-0 flex space-x-4"), - a( - href(m.url), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Otevřít" - ), - span(cls("text-gray-300"), "|"), - a( - href("#"), - cls("font-medium text-indigo-600 hover:text-indigo-500"), - "Odebrat" - ) + a( + href(m.url), + cls("font-medium text-indigo-600 hover:text-indigo-500"), + "Otevřít" ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala index 554515d..12bbac9 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileList.scala @@ -2,23 +2,11 @@ import com.raquo.laminar.api.L.{*, given} import cz.e_bs.cmi.mdr.pdb.app.components.Icons +import io.laminext.syntax.core.{*, given} -case class FileList( - files: Signal[List[File]] -) - -object FileList: - extension (m: FileList) - def toHtml: HtmlElement = - div( - ul( - role("list"), - cls("border border-gray-200 rounded-md divide-y divide-gray-200"), - children <-- m.files.map(_.map(_.toHtml)) - ), - button( - tpe := "button", - cls := "mt-5 bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Přidat soubor" - ) - ) +def FileList(files: List[File]): HtmlElement = + ul( + role("list"), + cls("border border-gray-200 rounded-md divide-y divide-gray-200"), + files.map(_.toHtml) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala index 6fdbaa2..b4e506a 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FilePicker.scala @@ -1,13 +1,15 @@ package cz.e_bs.cmi.mdr.pdb.app.components.files import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} object FilePicker: import cz.e_bs.cmi.mdr.pdb.app.components.files - val File = files.File - val Selector = FileSelector _ - def apply(display: Signal[List[File]] => HtmlElement): HtmlElement = + val (fsaStream, fsaObserver) = EventStream.withObserver[FileSelector.Action] + val selectorOpen = Var[Boolean](false) + + def apply(chosenFiles: Var[List[File]]): HtmlElement = // This sequence tricks browser into displaying modal content centered // Inspired by modal in headless ui playground // https://github.com/tailwindlabs/headlessui/blob/fdd26297953080d5ec905dda0bf5ec9607897d86/packages/playground-react/pages/transitions/component-examples/modal.tsx#L78-L79 @@ -29,43 +31,62 @@ */ div( div( + onClick.mapTo(false) --> selectorOpen.writer, cls("fixed inset-0 transition-opacity"), div(cls("absolute inset-0 bg-gray-500 opacity-75")) ) ) - val selectedFiles = Var[Set[File]](Set.empty) - - div( + inline def modalSelector: HtmlElement = div( cls("fixed inset-0 z-10 overflow-y-auto"), div( - cls( - "text-center sm:block sm:p-0" - ), + cls("text-center sm:block sm:p-0"), overlay, browserCenteringModalTrick, - Selector( - Val( - List( - File( - "https://tc163.cmi.cz/first_file", - "Smlouva o pracovním poměru" - ), - File( - "https://tc163.cmi.cz/first_file", - "Vysokoškolský diplom" - ), - File( - "https://tc163.cmi.cz/first_file", - "Prezenční listina školení" - ), - File("https://tc163.cmi.cz/first_file", "Životopis") + child <-- chosenFiles.signal.map( + FileSelector( + _, + Val( + List( + File( + "https://tc163.cmi.cz/first_file", + "Smlouva o pracovním poměru" + ), + File( + "https://tc163.cmi.cz/first_file", + "Vysokoškolský diplom" + ), + File( + "https://tc163.cmi.cz/first_file", + "Prezenční listina školení" + ), + File("https://tc163.cmi.cz/first_file", "Životopis") + ) ) - ), - selectedFiles + )(fsaObserver) ) ) + ) + + div( + fsaStream.collect { case FileSelector.SelectionUpdated(files) => + files.to(List) + } --> chosenFiles.writer, + fsaStream.mapTo(false) --> selectorOpen.writer, + child.maybe <-- selectorOpen.signal.map(isOpen => + if isOpen then Some(modalSelector) else None ), - display(selectedFiles.signal.map(_.to(List))) + div( + cls("flex flex-col space-y-5"), + child.maybe <-- chosenFiles.signal.map(files => + if files.isEmpty then None else Some(FileList(files)) + ), + button( + tpe := "button", + cls := "bg-white py-2 px-3 border border-gray-300 rounded-md shadow-sm text-sm leading-4 font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zvolit soubory", + onClick.mapTo(true) --> selectorOpen.writer + ) + ) ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala index 3822cc1..a14d16d 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileSelector.scala @@ -6,138 +6,62 @@ import io.laminext.syntax.core.{*, given} import com.raquo.domtypes.generic.codecs.BooleanAsTrueFalseStringCodec -def FileTable( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - val scope = customHtmlAttr("scope", StringAsIsCodec) - - def headerRow: HtmlElement = - val col = scope("col") - val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) - val textH = cls( - "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" - ) - tr( - th(baseM, span(cls("sr-only"), "Vybrat")), - th(baseM, textH, "Název"), - th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) - ) - - def tableRow( - f: File, - idx: Int, - selected: Boolean - )(toggleSelection: Observer[Unit]): HtmlElement = - val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") - tr( - cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), - td( - cls("font-medium cursor-pointer"), - onClick.mapTo(()) --> toggleSelection, - cls(if selected then "text-green-900" else "text-gray-200"), - Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), - span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") - ), - td( - baseC, - cls("font-medium text-gray-900"), - f.name, - onClick.mapTo(()) --> toggleSelection - ), - td( - baseC, - cls("text-right font-medium"), - a( - href(f.url), - target("_blank"), - cls( - "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" - ), - Icons.outline.`external-link`(), - "Otevřít" - ) - ) - ) - - div( - cls("flex flex-col"), - div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), +object FileSelector: + sealed trait Action + case class SelectionUpdated(files: Set[File]) extends Action + case object SelectionCancelled extends Action + def apply(initialFiles: List[File], availableFiles: Signal[List[File]])( + selectionDone: Observer[Action] + ): HtmlElement = + val selectedFiles = Var[Set[File]](initialFiles.to(Set)) div( - cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + cls( + "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" + ), + role("dialog"), + customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), + aria.labelledBy("modal-headline"), div( - cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), - table( - cls("min-w-full divide-y divide-gray-200"), - thead( - cls("bg-gray-50"), - headerRow - ), - tbody( - children <-- files - .map(_.zipWithIndex) - .combineWithFn(selectedFiles)((f, sel) => - f.map((file, idx) => - val active = sel.contains(file) - tableRow(file, idx, active)( - selectedFiles.writer - .contramap(_ => if active then sel - file else sel + file) - ) - ) - ) - ) - ) - ) - ) - ) - -def FileSelector( - files: Signal[List[File]], - selectedFiles: Var[Set[File]] -): HtmlElement = - div( - cls( - "inline-block transform overflow-hidden rounded-lg bg-white text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:align-middle" - ), - role("dialog"), - customHtmlAttr("aria.modal", BooleanAsTrueFalseStringCodec)(true), - aria.labelledBy("modal-headline"), - div( - cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), - div( - cls("sm:flex sm:items-start"), + cls("bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4"), div( - cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), - h3( - cls("text-lg font-medium leading-6 text-gray-900"), - idAttr("modal-headline"), - "Výběr souborů" + cls("sm:flex sm:items-start"), + div( + cls("mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left"), + h3( + cls("text-lg font-medium leading-6 text-gray-900"), + idAttr("modal-headline"), + "Výběr souborů" + ) ) - ) + ), + FileTable(availableFiles, selectedFiles) ), - FileTable(files, selectedFiles) - ), - div( - cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), - span( - cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" - ), - "Potvrdit" - ) - ), - span( - cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), - button( - typ("button"), - cls( - "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" - ), - "Zrušit" + div( + cls("bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6"), + span( + cls("flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-green inline-flex w-full justify-center rounded-md border border-transparent bg-indigo-600 px-4 py-2 text-base font-medium leading-6 text-white shadow-sm transition duration-150 ease-in-out hover:bg-indigo-500 focus:border-indigo-700 focus:outline-none sm:text-sm sm:leading-5" + ), + "Potvrdit", + composeEvents(onClick)( + _.sample(selectedFiles) + .map(SelectionUpdated(_)) + ) --> selectionDone + ) + ), + span( + cls("mt-3 flex w-full rounded-md shadow-sm sm:mt-0 sm:w-auto"), + button( + typ("button"), + cls( + "focus:shadow-outline-blue inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium leading-6 text-gray-700 shadow-sm transition duration-150 ease-in-out hover:text-gray-500 focus:border-blue-300 focus:outline-none sm:text-sm sm:leading-5" + ), + "Zrušit", + onClick.mapTo(SelectionCancelled) --> selectionDone + ) ) ) ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala new file mode 100644 index 0000000..0fcc458 --- /dev/null +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/files/FileTable.scala @@ -0,0 +1,90 @@ +package cz.e_bs.cmi.mdr.pdb.app.components.files + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import cz.e_bs.cmi.mdr.pdb.app.components.Icons + +def FileTable( + files: Signal[List[File]], + selectedFiles: Var[Set[File]] +): HtmlElement = + val scope = customHtmlAttr("scope", StringAsIsCodec) + + def headerRow: HtmlElement = + val col = scope("col") + val baseM: Modifier[HtmlElement] = Seq(cls("px-6 py-3"), col) + val textH = cls( + "text-left text-xs font-medium text-gray-500 uppercase tracking-wider" + ) + tr( + th(baseM, span(cls("sr-only"), "Vybrat")), + th(baseM, textH, "Název"), + th(baseM, cls("relative"), span(cls("sr-only"), "Otevřít")) + ) + + def tableRow( + f: File, + idx: Int, + selected: Boolean + )(toggleSelection: Observer[Unit]): HtmlElement = + val baseC = cls("px-6 py-4 whitespace-nowrap text-sm") + tr( + cls(if idx % 2 == 0 then "bg-gray-50" else "bg-white"), + td( + cls("font-medium cursor-pointer"), + onClick.mapTo(()) --> toggleSelection, + cls(if selected then "text-green-900" else "text-gray-200"), + Icons.outline.`check-circle`().amend(svg.cls := "mx-auto"), + span(cls("sr-only"), if selected then "Vybráno" else "Nevybráno") + ), + td( + baseC, + cls("font-medium text-gray-900"), + f.name, + onClick.mapTo(()) --> toggleSelection + ), + td( + baseC, + cls("text-right font-medium"), + a( + href(f.url), + target("_blank"), + cls( + "flex items-center space-x-4 text-indigo-600 hover:text-indigo-900" + ), + Icons.outline.`external-link`(), + "Otevřít" + ) + ) + ) + + div( + cls("flex flex-col"), + div(cls("overflow-x-auto sm:-mx-6 lg:-mx-8")), + div( + cls("py-2 align-middle inline-block min-w-full sm:px-6 lg:px-8"), + div( + cls("shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"), + table( + cls("min-w-full divide-y divide-gray-200"), + thead( + cls("bg-gray-50"), + headerRow + ), + tbody( + children <-- files + .map(_.zipWithIndex) + .combineWithFn(selectedFiles)((f, sel) => + f.map((file, idx) => + val active = sel.contains(file) + tableRow(file, idx, active)( + selectedFiles.writer + .contramap(_ => if active then sel - file else sel + file) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala index 7761871..e03f8b3 100644 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -8,6 +8,9 @@ import com.raquo.laminar.nodes.ReactiveHtmlElement import cz.e_bs.cmi.mdr.pdb.app.components.files import cz.e_bs.cmi.mdr.pdb.app.components.files.FilePicker +import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz +import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef +import cz.e_bs.cmi.mdr.pdb.app.components.files.File object UpravDukazForm: object SubmitButtons: @@ -30,6 +33,7 @@ ) def apply(): HtmlElement = + val files = Var[List[File]](Nil) div( cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", Form( @@ -45,11 +49,18 @@ FormRow( "dokumenty", "Dokumenty", - FilePicker( - files - .FileList(_) - .toHtml - .amend(idAttr := "dokumenty", cls("max-w-lg")) + FilePicker(files) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" ) ).toHtml, FormRow(