I need to order pages (defined by user, drag and drop), something like ("Hello", order 1), ("Bye", order 2)
And then insert, let's say ("Good", order 1)
, so the collection would change to ("Good", order 1), ("Hello", order 2), ("Bye", order 3)
.
Any ideas how to achieve this with IHP/Postgres?
I have looked at
https://begriffs.com/posts/2018-03-20-user-defined-order.html
Are those the best solutions we have to date?
I usually use the integer position
column approach. Here's an example code from a real world IHP app:
In this case we have offers
column with a position
integer column, like this:
CREATE TABLE offers (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
name TEXT NOT NULL,
"location" TEXT NOT NULL,
contact TEXT NOT NULL,
description TEXT NOT NULL,
"position" INT NOT NULL
);
The controller looks like this:
module Web.Controller.Offers where
import Web.Controller.Prelude
import Web.View.Offers.Edit
import Web.View.Offers.Show
import Web.View.Offers.New
import Web.View.Offers.Index
import qualified Application.Offer as Offer
import qualified Control.Monad.State.Lazy as State
instance Controller OffersController where
beforeAction = ensureIsUser
action NewOfferAction = do
let backField :: Offer = newRecord
render NewView { .. }
action OffersAction = do
backFields <- query @Offer |> orderBy #position |> fetch
render IndexView { .. }
action ShowOfferAction { .. } = do
backField <- fetch backFieldId
render ShowView { .. }
action EditOfferAction { .. } = do
backField <- fetch backFieldId
render EditView { .. }
action UpdateOfferAction { .. } = do
backField <- fetch backFieldId
backField
|> buildOffer
|> ifValid \case
Left backField -> render EditView { .. }
Right backField -> do
backField <- updateRecord backField
setSuccessMessage "Offer updated"
redirectTo EditOfferAction { .. }
action CreateOfferAction = do
nextPosition <- Offer.nextPosition
newRecord @Offer
|> buildOffer
|> ifValid \case
Left backField -> render NewView { .. }
Right backField -> do
backField <- backField
|> createRecord
setSuccessMessage "Angebot erstellt"
redirectTo OffersAction
action DeleteOfferAction { .. } = do
backField <- fetch backFieldId
deleteRecord backField
setSuccessMessage "Deleted Offer successfully"
redirectTo OffersAction
action OfferMoveUpAction { .. } = do
backField <- fetch backFieldId
prevOffer <- backField |> Offer.prevOffer
case prevOffer of
Just prevOffer -> do
let backFieldPosition = get #position backField
let prevOfferPosition = get #position prevOffer
updateRecord (backField |> set #position prevOfferPosition)
updateRecord (prevOffer |> set #position backFieldPosition)
return ()
Nothing -> return ()
redirectTo OffersAction
action OfferMoveDownAction { .. } = do
backField <- fetch backFieldId
nextOffer <- backField |> Offer.nextOffer
case nextOffer of
Just nextOffer -> do
let backFieldPosition = get #position backField
let nextOfferPosition = get #position nextOffer
updateRecord (backField |> set #position nextOfferPosition)
updateRecord (nextOffer |> set #position backFieldPosition)
return ()
Nothing -> return ()
redirectTo OffersAction
buildOffer :: _ => backField -> backField
buildOffer backField =
backField
|> fill @'["name", "description", "position", "location", "contact"]
|> validateField #name nonEmpty
Some helper functions used from the controller are defined inside another module (but you could just put them into the controller if you want):
module Application.Offer (nextPosition, prevOffer, nextOffer) where
import IHP.Prelude
import IHP.ModelSupport
import IHP.QueryBuilder
import Generated.Types
import qualified Database.PostgreSQL.Simple as PG
instance DefaultScope "offers" where
defaultScope = orderBy #position
nextPosition :: (?modelContext :: ModelContext) => IO Int
nextPosition = sqlQueryScalar "SELECT COUNT(*) FROM offers" ()
prevOffer :: (?modelContext :: ModelContext) => Offer -> IO (Maybe Offer)
prevOffer offer = do
results <- sqlQuery "SELECT * FROM offers WHERE position < ? ORDER BY position DESC LIMIT 1" (PG.Only (get #position offer))
return $ headMay results
nextOffer :: (?modelContext :: ModelContext) => Offer -> IO (Maybe Offer)
nextOffer offer = do
results <- sqlQuery "SELECT * FROM offers WHERE position > ? ORDER BY position ASC LIMIT 1" (PG.Only (get #position offer))
return $ headMay results