« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
path: root/src/data/things/homepage-layout.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/data/things/homepage-layout.js')
-rw-r--r--src/data/things/homepage-layout.js314
1 files changed, 214 insertions, 100 deletions
diff --git a/src/data/things/homepage-layout.js b/src/data/things/homepage-layout.js
index 00d6aef5..3a11c287 100644
--- a/src/data/things/homepage-layout.js
+++ b/src/data/things/homepage-layout.js
@@ -1,8 +1,11 @@
 export const HOMEPAGE_LAYOUT_DATA_FILE = 'homepage.yaml';
 
+import {inspect} from 'node:util';
+
+import {colors} from '#cli';
 import {input} from '#composite';
-import find from '#find';
 import Thing from '#thing';
+import {empty} from '#sugar';
 
 import {
   anyOf,
@@ -11,19 +14,26 @@ import {
   isString,
   isStringNonEmpty,
   validateArrayItems,
-  validateInstanceOf,
   validateReference,
 } from '#validators';
 
 import {exposeDependency} from '#composite/control-flow';
 import {withResolvedReference} from '#composite/wiki-data';
-import {color, contentString, name, referenceList, wikiData}
-  from '#composite/wiki-properties';
+
+import {
+  color,
+  contentString,
+  name,
+  referenceList,
+  soupyFind,
+  thing,
+  thingList,
+} from '#composite/wiki-properties';
 
 export class HomepageLayout extends Thing {
   static [Thing.friendlyName] = `Homepage Layout`;
 
-  static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({
+  static [Thing.getPropertyDescriptors] = ({HomepageLayoutSection}) => ({
     // Update & expose
 
     sidebarContent: contentString(),
@@ -31,15 +41,12 @@ export class HomepageLayout extends Thing {
     navbarLinks: {
       flags: {update: true, expose: true},
       update: {validate: validateArrayItems(isStringNonEmpty)},
+      expose: {transform: value => value ?? []},
     },
 
-    rows: {
-      flags: {update: true, expose: true},
-
-      update: {
-        validate: validateArrayItems(validateInstanceOf(HomepageLayoutRow)),
-      },
-    },
+    sections: thingList({
+      class: input.value(HomepageLayoutSection),
+    }),
   });
 
   static [Thing.yamlDocumentSpec] = {
@@ -50,85 +57,230 @@ export class HomepageLayout extends Thing {
       'Navbar Links': {property: 'navbarLinks'},
     },
   };
+
+  static [Thing.getYamlLoadingSpec] = ({
+    documentModes: {allInOne},
+    thingConstructors: {
+      HomepageLayout,
+      HomepageLayoutSection,
+    },
+  }) => ({
+    title: `Process homepage layout file`,
+    file: HOMEPAGE_LAYOUT_DATA_FILE,
+
+    documentMode: allInOne,
+    documentThing: document => {
+      if (document['Homepage']) {
+        return HomepageLayout;
+      }
+
+      if (document['Section']) {
+        return HomepageLayoutSection;
+      }
+
+      if (document['Row']) {
+        switch (document['Row']) {
+          case 'actions':
+            return HomepageLayoutActionsRow;
+          case 'album carousel':
+            return HomepageLayoutAlbumCarouselRow;
+          case 'album grid':
+            return HomepageLayoutAlbumGridRow;
+          default:
+            throw new TypeError(`Unrecognized row type ${document['Row']}`);
+        }
+      }
+
+      return null;
+    },
+
+    save(results) {
+      if (!empty(results) && !(results[0] instanceof HomepageLayout)) {
+        throw new Error(`Expected 'Homepage' document at top of homepage layout file`);
+      }
+
+      const homepageLayout = results[0];
+      const sections = [];
+
+      let currentSection = null;
+      let currentSectionRows = [];
+
+      const closeCurrentSection = () => {
+        if (currentSection) {
+          for (const row of currentSectionRows) {
+            row.section = currentSection;
+          }
+
+          currentSection.rows = currentSectionRows;
+          sections.push(currentSection);
+
+          currentSection = null;
+          currentSectionRows = [];
+        }
+      };
+
+      for (const entry of results.slice(1)) {
+        if (entry instanceof HomepageLayout) {
+          throw new Error(`Expected only one 'Homepage' document in total`);
+        } else if (entry instanceof HomepageLayoutSection) {
+          closeCurrentSection();
+          currentSection = entry;
+        } else if (entry instanceof HomepageLayoutRow) {
+          if (currentSection) {
+            currentSectionRows.push(entry);
+          } else {
+            throw new Error(`Expected a 'Section' document to add following rows into`);
+          }
+        }
+      }
+
+      closeCurrentSection();
+
+      homepageLayout.sections = sections;
+
+      return {homepageLayout};
+    },
+  });
+}
+
+export class HomepageLayoutSection extends Thing {
+  static [Thing.friendlyName] = `Homepage Section`;
+
+  static [Thing.getPropertyDescriptors] = ({HomepageLayoutRow}) => ({
+    // Update & expose
+
+    name: name(`Unnamed Homepage Section`),
+
+    color: color(),
+
+    rows: thingList({
+      class: input.value(HomepageLayoutRow),
+    }),
+  });
+
+  static [Thing.yamlDocumentSpec] = {
+    fields: {
+      'Section': {property: 'name'},
+      'Color': {property: 'color'},
+    },
+  };
 }
 
 export class HomepageLayoutRow extends Thing {
   static [Thing.friendlyName] = `Homepage Row`;
 
-  static [Thing.getPropertyDescriptors] = ({Album, Group}) => ({
+  static [Thing.getPropertyDescriptors] = ({HomepageLayoutSection}) => ({
     // Update & expose
 
-    name: name('Unnamed Homepage Row'),
+    section: thing({
+      class: input.value(HomepageLayoutSection),
+    }),
+
+    // Update only
+
+    find: soupyFind(),
+
+    // Expose only
 
     type: {
-      flags: {update: true, expose: true},
+      flags: {expose: true},
 
-      update: {
-        validate() {
+      expose: {
+        compute() {
           throw new Error(`'type' property validator must be overridden`);
         },
       },
     },
+  });
 
-    color: color(),
+  static [Thing.yamlDocumentSpec] = {
+    fields: {
+      'Row': {ignore: true},
+    },
+  };
 
-    // Update only
+  [inspect.custom](depth) {
+    const parts = [];
 
-    // These wiki data arrays aren't necessarily used by every subclass, but
-    // to the convenience of providing these, the superclass accepts all wiki
-    // data arrays depended upon by any subclass.
+    parts.push(Thing.prototype[inspect.custom].apply(this));
 
-    albumData: wikiData({
-      class: input.value(Album),
-    }),
+    if (depth >= 0 && this.section) {
+      const sectionName = this.section.name;
+      const index = this.section.rows.indexOf(this);
+      const rowNum =
+        (index === -1
+          ? 'indeterminate position'
+          : `#${index + 1}`);
+      parts.push(` (${colors.yellow(rowNum)} in ${colors.green(sectionName)})`);
+    }
 
-    groupData: wikiData({
-      class: input.value(Group),
-    }),
+    return parts.join('');
+  }
+}
+
+export class HomepageLayoutActionsRow extends HomepageLayoutRow {
+  static [Thing.friendlyName] = `Homepage Actions Row`;
+
+  static [Thing.getPropertyDescriptors] = (opts) => ({
+    ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts),
+
+    // Update & expose
+
+    actionLinks: {
+      flags: {update: true, expose: true},
+      update: {validate: validateArrayItems(isString)},
+    },
+
+    // Expose only
+
+    type: {
+      flags: {expose: true},
+      expose: {compute: () => 'actions'},
+    },
   });
 
-  static [Thing.yamlDocumentSpec] = {
+  static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(HomepageLayoutRow, {
     fields: {
-      'Row': {property: 'name'},
-      'Color': {property: 'color'},
-      'Type': {property: 'type'},
+      'Actions': {property: 'actionLinks'},
     },
-  };
+  });
 }
 
-export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
-  static [Thing.friendlyName] = `Homepage Albums Row`;
+export class HomepageLayoutAlbumCarouselRow extends HomepageLayoutRow {
+  static [Thing.friendlyName] = `Homepage Album Carousel Row`;
 
-  static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({
+  static [Thing.getPropertyDescriptors] = (opts, {Album} = opts) => ({
     ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts),
 
     // Update & expose
 
+    albums: referenceList({
+      class: input.value(Album),
+      find: soupyFind.input('album'),
+    }),
+
+    // Expose only
+
     type: {
-      flags: {update: true, expose: true},
-      update: {
-        validate(value) {
-          if (value !== 'albums') {
-            throw new TypeError(`Expected 'albums'`);
-          }
+      flags: {expose: true},
+      expose: {compute: () => 'album carousel'},
+    },
+  });
 
-          return true;
-        },
-      },
+  static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(HomepageLayoutRow, {
+    fields: {
+      'Albums': {property: 'albums'},
     },
+  });
+}
 
-    displayStyle: {
-      flags: {update: true, expose: true},
+export class HomepageLayoutAlbumGridRow extends HomepageLayoutRow {
+  static [Thing.friendlyName] = `Homepage Album Grid Row`;
 
-      update: {
-        validate: is('grid', 'carousel'),
-      },
+  static [Thing.getPropertyDescriptors] = (opts, {Album, Group} = opts) => ({
+    ...HomepageLayoutRow[Thing.getPropertyDescriptors](opts),
 
-      expose: {
-        transform: (displayStyle) =>
-          displayStyle ?? 'grid',
-      },
-    },
+    // Update & expose
 
     sourceGroup: [
       {
@@ -151,8 +303,7 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
 
       withResolvedReference({
         ref: input.updateValue(),
-        data: 'groupData',
-        find: input.value(find.group),
+        find: soupyFind.input('group'),
       }),
 
       exposeDependency({dependency: '#resolvedReference'}),
@@ -160,8 +311,7 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
 
     sourceAlbums: referenceList({
       class: input.value(Album),
-      find: input.value(find.album),
-      data: 'albumData',
+      find: soupyFind.input('album'),
     }),
 
     countAlbumsFromGroup: {
@@ -169,55 +319,19 @@ export class HomepageLayoutAlbumsRow extends HomepageLayoutRow {
       update: {validate: isCountingNumber},
     },
 
-    actionLinks: {
-      flags: {update: true, expose: true},
-      update: {validate: validateArrayItems(isString)},
+    // Expose only
+
+    type: {
+      flags: {expose: true},
+      expose: {compute: () => 'album grid'},
     },
   });
 
   static [Thing.yamlDocumentSpec] = Thing.extendDocumentSpec(HomepageLayoutRow, {
     fields: {
-      'Display Style': {property: 'displayStyle'},
       'Group': {property: 'sourceGroup'},
       'Count': {property: 'countAlbumsFromGroup'},
       'Albums': {property: 'sourceAlbums'},
-      'Actions': {property: 'actionLinks'},
-    },
-  });
-
-  static [Thing.getYamlLoadingSpec] = ({
-    documentModes: {headerAndEntries}, // Kludge, see below
-    thingConstructors: {
-      HomepageLayout,
-      HomepageLayoutAlbumsRow,
-    },
-  }) => ({
-    title: `Process homepage layout file`,
-
-    // Kludge: This benefits from the same headerAndEntries style messaging as
-    // albums and tracks (for example), but that document mode is designed to
-    // support multiple files, and only one is actually getting processed here.
-    files: [HOMEPAGE_LAYOUT_DATA_FILE],
-
-    documentMode: headerAndEntries,
-    headerDocumentThing: HomepageLayout,
-    entryDocumentThing: document => {
-      switch (document['Type']) {
-        case 'albums':
-          return HomepageLayoutAlbumsRow;
-        default:
-          throw new TypeError(`No processDocument function for row type ${document['Type']}!`);
-      }
-    },
-
-    save(results) {
-      if (!results[0]) {
-        return;
-      }
-
-      const {header: homepageLayout, entries: rows} = results[0];
-      Object.assign(homepageLayout, {rows});
-      return {homepageLayout};
     },
   });
 }