anyone-oslo/pages

View on GitHub
app/javascript/components/PageForm/usePage.ts

Summary

Maintainability
A
25 mins
Test Coverage
import { useReducer } from "react";

import * as PageEditor from "../../types/PageEditor";
import * as Pages from "../../types/Pages";
import * as Template from "../../types/Template";
import { LocalizedValue, MaybeLocalizedValue } from "../../types";

export function blockValue(
  state: PageEditor.State,
  block: Template.Block
): string {
  if (block.localized) {
    const value: LocalizedValue =
      (state.page.blocks[block.name] as LocalizedValue) || {};

    return value[state.locale] || "";
  } else {
    return (state.page.blocks[block.name] as string) || "";
  }
}

export function errorsOn(page: Pages.Resource, attribute: string): string[] {
  return page.errors
    .filter((e) => e.attribute === attribute)
    .map((e) => e.message);
}

export function unconfiguredBlocks(state: PageEditor.State): Template.Block[] {
  const allBlocks: Record<string, Template.Block> = state.templates
    .flatMap((t) => t.blocks)
    .reduce((bs, b) => ({ [b.name]: b, ...bs }), {});

  const anyValue = (v: MaybeLocalizedValue) => {
    if (typeof v === "string") {
      return v ? true : false;
    } else {
      return Object.values(v).filter((v) => v).length > 0;
    }
  };

  const hasValue = Object.keys(allBlocks).filter((k) => {
    const value = state.page.blocks[k] || "";
    return anyValue(value);
  });

  const enabled = state.templateConfig.blocks.map((b) => b.name);

  return hasValue
    .filter((b) => enabled.indexOf(b) === -1)
    .map((n) => allBlocks[n]);
}

function parseDate(str: string): Date | null {
  if (!str) {
    return null;
  } else if (typeof str === "string") {
    return new Date(str);
  } else {
    return str;
  }
}

function derivedState(state: PageEditor.State): PageEditor.State {
  const { locale, locales, page, templates } = state;
  return {
    ...state,
    inputDir: (locales && locales[locale] && locales[locale].dir) || "ltr",
    templateConfig: templates.filter(
      (t) => t.template_name === page.template
    )[0]
  };
}

function parsedDates(page: Pages.SerializedResource) {
  return {
    published_at: parseDate(page.published_at),
    starts_at: parseDate(page.starts_at),
    ends_at: parseDate(page.ends_at)
  };
}

function localizedAttributes(templates: Template.Config[]): string[] {
  const allBlocks = (t: Template.Config): Template.Block[] => {
    return [...t.blocks, ...t.metadata_blocks];
  };

  const blockNames = templates
    .map(allBlocks)
    .reduce((acc, val) => acc.concat(val), [])
    .filter((b) => b.localized)
    .map((b) => b.name)
    .filter((value, index, array) => array.indexOf(value) === index);

  return ["path_segment", ...blockNames];
}

function prepare(
  state: PageEditor.State<Pages.SerializedResource>
): PageEditor.State {
  const page = { ...state.page, ...parsedDates(state.page) };
  return { ...state, page: page, datesEnabled: page.starts_at ? true : false };
}

function reducer(
  state: PageEditor.State,
  action: PageEditor.Action
): PageEditor.State {
  const { type, payload } = action;
  switch (type) {
    case "setPage":
      return prepare({ ...state, page: payload });
    case "setDatesEnabled":
      return { ...state, datesEnabled: payload };
    case "setLocale":
      return { ...state, locale: payload };
    case "update":
      return updatePage(state, payload);
    case "updateBlocks":
      return updatePageBlocks(state, payload);
    default:
      return state;
  }
}

function updateLocalized<T>(
  state: PageEditor.State,
  obj: T,
  attributes: Partial<T>
): T {
  const { locale, templates } = state;
  const nextObj = {};

  Object.keys(attributes).forEach((attr: string) => {
    const value = attributes[attr] as MaybeLocalizedValue;
    if (localizedAttributes(templates).indexOf(attr) !== -1) {
      nextObj[attr] = { ...obj[attr], [locale]: value } as LocalizedValue;
    } else {
      nextObj[attr] = value;
    }
  });

  return { ...obj, ...nextObj };
}

function updatePageBlocks(
  state: PageEditor.State,
  attributes: Partial<Pages.Blocks>
): PageEditor.State {
  const { page } = state;

  return {
    ...state,
    page: { ...page, blocks: updateLocalized(state, page.blocks, attributes) }
  };
}

function updatePage(
  state: PageEditor.State,
  attributes: Partial<Pages.Resource>
): PageEditor.State {
  return { ...state, page: updateLocalized(state, state.page, attributes) };
}

export default function usePage(
  initialState: PageEditor.State<Pages.SerializedResource>
): [PageEditor.State, (action: PageEditor.Action) => void] {
  const [state, dispatch] = useReducer(reducer, prepare(initialState));
  return [derivedState(state), dispatch];
}