OurBigBook logoOurBigBook Docs OurBigBook logoOurBigBook.comSite Source code
web/pages/go/site-settings.tsx
import Router from 'next/router'
import React, { useRef } from 'react'

import { cant } from 'front/cant'
import {
  docsAdminUrl,
  docsUrl,
  networkSlowMs,
} from 'front/config'
import {
  USER_FINISHED_TYPING_MS
} from 'ourbigbook/runtime_common'
import ErrorList from 'front/ErrorList'
import Label from 'front/Label'
import MapErrors from 'front/MapErrors'
import {
  disableButton,
  enableButton,
  MyHead,
  SettingsIcon,
  useCtrlEnterSubmit,
  useConfirmExitPage,
  PinnedArticleIcon,
  NON_NEGATIVE_INPUT_RE,
  WarningIcon,
  ErrorIcon,
} from 'front'
import { webApi } from 'front/api'
import routes from 'front/routes'
import { CommonPropsType } from 'front/types/CommonPropsType'
import { SiteType } from 'front/types/SiteType'

interface SiteSettingsProps extends CommonPropsType {
  site: SiteType;
}

function getArticleErrors(pinnedArticle, exists) {
  let newPinnedArticleOks, newPinnedArticleErrors
  if (pinnedArticle) {
    if (exists) {
      newPinnedArticleErrors = []
      newPinnedArticleOks = [`Article exists`]
    } else {
      newPinnedArticleErrors = [`Article does not exist`]
    }
  } else {
    newPinnedArticleErrors = []
    newPinnedArticleOks = [`No pinned article`]
  }
  return [newPinnedArticleErrors, newPinnedArticleOks]
}

export default function SiteSettings({
  loggedInUser,
  site: siteInit,
}: SiteSettingsProps) {
  const [loading, setLoading] = React.useState(false)
  const [errors, setErrors] = React.useState([])
  if (siteInit.pinnedArticle === undefined) {
    siteInit.pinnedArticle = ''
  }
  const [site, setSite] = React.useState(siteInit)
  const [siteLastSaved, setSiteLastSaved] = React.useState(siteInit)
  const [_, pinnedArticleOksInit] = getArticleErrors(siteInit.pinnedArticle, true)
  const [pinnedArticleOks, setPinnedArticleOks] = React.useState(pinnedArticleOksInit)
  const [pinnedArticleErrors, setPinnedArticleErrors] = React.useState([])
  const [automaticTopicLinksMaxWordsErrors, setAutomaticTopicLinksMaxWordsErrors] = React.useState([])
  const [pinnedArticleLoading, setPinnedArticleLoading] = React.useState(false)
  const [pinnedArticleCheckDone, setPinnedArticleCheckDone] = React.useState(true)
  const pinnedArticleI = React.useRef(0)
  let pinnedArticleIClosure = pinnedArticleI.current
  const [formChanged, setFormChanged] = React.useState(false)
  const updateState = (field) => async (e) => {
    let val = e.target.value
    let pinnedArticle
    let valueOk = true
    if (field === 'pinnedArticle') {
      pinnedArticle = val
      setPinnedArticleCheckDone(false)
      pinnedArticleI.current++
    } else if (field === 'automaticTopicLinksMaxWords') {
      e.preventDefault()
      if (val.match(NON_NEGATIVE_INPUT_RE)) {
        e.target.value = val
        if (val === '') {
          setAutomaticTopicLinksMaxWordsErrors(['Cannot be empty'])
        } else {
          val = Number(val)
          setAutomaticTopicLinksMaxWordsErrors([])
        }
      } else {
        valueOk = false
      }
    }
    if (valueOk) {
      const newSite = { ...site, [field]: val }
      setSite(newSite)
      setFormChanged(
        siteLastSaved.automaticTopicLinksMaxWords !== newSite.automaticTopicLinksMaxWords ||
        siteLastSaved.pinnedArticle !== newSite.pinnedArticle
      )
      if (pinnedArticle) {
        setTimeout(() => {
          if (pinnedArticleIClosure + 1 === pinnedArticleI.current) {
            let done = false
            setTimeout(() => {
              if (!done) { setPinnedArticleLoading(true) }
            }, networkSlowMs)
            webApi.article(pinnedArticle).then(ret => {
              if (pinnedArticleIClosure + 1 === pinnedArticleI.current) {
                const articleExists = !!ret.data
                setPinnedArticleLoading(false)
                const [newPinnedArticleErrors, newPinnedArticleOks] = getArticleErrors(
                  pinnedArticle,
                  articleExists
                )
                setPinnedArticleErrors(newPinnedArticleErrors)
                setPinnedArticleOks(newPinnedArticleOks)
                setPinnedArticleCheckDone(true)
                done = true
              }
            })
          }
        }, USER_FINISHED_TYPING_MS)
      } else {
        // Empty is always valid, so we make no request.
        const [newPinnedArticleErrors, newPinnedArticleOks] = getArticleErrors(
          pinnedArticle,
          true
        )
        setPinnedArticleErrors(newPinnedArticleErrors)
        setPinnedArticleOks(newPinnedArticleOks)
        setPinnedArticleCheckDone(true)
      }
    }
  }
  const handleSubmit = async (e) => {
    if (hasError) {
      return
    }
    e.preventDefault()
    setLoading(true)
    const { data, status } = await webApi.siteSettingsUpdate(site)
    setLoading(false)
    if (status !== 200) {
      setErrors(data.errors)
    }
    setSiteLastSaved({ ...site })
    setFormChanged(false)
  }
  useCtrlEnterSubmit(handleSubmit)
  useConfirmExitPage(!formChanged)
  const title = 'Site settings'
  const canUpdate = !cant.updateSiteSettings(loggedInUser)
  const submitElem = useRef(null)
  const hasError = 
      pinnedArticleErrors.length ||
      automaticTopicLinksMaxWordsErrors.length
  if (submitElem.current) {
    if (pinnedArticleCheckDone && !hasError) {
      enableButton(submitElem.current)
    } else {
      disableButton(submitElem.current)
    }
  }
  return <>
    <MyHead title={title} />
    <div className="settings-page content-not-ourbigbook">
      <h1><SettingsIcon /> {title}</h1>
      <p>This page contains global settings that affect the entire website. It can only be edited by <a href={`${docsAdminUrl}`}>admins</a>.</p>
      <MapErrors errors={errors} />
      <form onSubmit={handleSubmit}>
        <Label
          label={<>
            <PinnedArticleIcon /> Pinned article
            {canUpdate &&
              <ErrorList
                errors={pinnedArticleErrors}
                oks={pinnedArticleOks}
                inline={true}
                loading={pinnedArticleLoading}
              />
            }
          </>}
        >
          <input
            type="text"
            placeholder={"(currently empty) Sample value: \"user0/article0\". Empty for don't pin any."}
            value={site.pinnedArticle}
            onChange={updateState('pinnedArticle')}
            disabled={!canUpdate}
          />
        </Label>
        <Label label={<>
          Maximum number of words for automatic topic links (<a href={`${docsUrl}/automatic-topic-linking`}>automaticTopicLinksMaxWords</a>)
          <ErrorList
            errors={automaticTopicLinksMaxWordsErrors}
            inline={true}
          />
        </>} >
          <input
            placeholder={"(currently empty) 0: turn off automatic topic links >0: turn on and use this many words at most per link"}
            value={site.automaticTopicLinksMaxWords.toString()}
            onChange={updateState('automaticTopicLinksMaxWords')}
            disabled={!canUpdate}
          />
        </Label>
        {(canUpdate) &&
          <div className="submit-container">
            <button
              className="btn"
              type="submit"
              ref={submitElem}
            >
              Update settings
            </button>
            {hasError
              ? <span className="message"><ErrorIcon /> Cannot submit due to errors</span>
              : formChanged && <>
                  <span className="message"><WarningIcon /> Unsaved changes</span>
                </>
            }
          </div>
        }
      </form>
    </div>
  </>
}

import { getLoggedInUser } from 'back'

export async function getServerSideProps(context) {
  const { req, res } = context
  const sequelize = req.sequelize
  const [loggedInUser, site] = await Promise.all([
    getLoggedInUser(req, res),
    sequelize.models.Site.findOne(),
  ])
  const [siteJson, loggedInUserJson] = await Promise.all([
    site.toJson(loggedInUser),
    loggedInUser ? loggedInUser.toJson(loggedInUser) : null,
  ])
  return {
    props: {
      loggedInUser: loggedInUserJson,
      site: siteJson,
    }
  }
}