Skip to content

Commit

Permalink
feat(lib): support flat answers
Browse files Browse the repository at this point in the history
BREAKING CHANGE: The whole lib layer changed since we moved to the new
API structure. However, the cf-content component still works the same
way as before.
  • Loading branch information
Jonas Metzener committed Jul 2, 2019
1 parent 0b863d8 commit b602056
Show file tree
Hide file tree
Showing 84 changed files with 2,343 additions and 2,632 deletions.
6 changes: 0 additions & 6 deletions addon/-private/fragment-types.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ export default {
{
name: "Document"
},
{
name: "FormAnswer"
},
{
name: "Case"
},
Expand Down Expand Up @@ -167,9 +164,6 @@ export default {
kind: "INTERFACE",
name: "Answer",
possibleTypes: [
{
name: "FormAnswer"
},
{
name: "StringAnswer"
},
Expand Down
309 changes: 78 additions & 231 deletions addon/components/cf-content.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,19 @@
import Component from "@ember/component";
import layout from "../templates/components/cf-content";
import { inject as service } from "@ember/service";
import { computed, observer } from "@ember/object";
import { reads, filterBy } from "@ember/object/computed";
import { computed } from "@ember/object";
import { reads } from "@ember/object/computed";
import { ComponentQueryManager } from "ember-apollo-client";
import { task } from "ember-concurrency";
import { later, once } from "@ember/runloop";
import Document from "ember-caluma/lib/document";
import Navigation from "ember-caluma/lib/navigation";
import { parseDocument } from "ember-caluma/lib/parsers";

import getNavigationDocumentsQuery from "ember-caluma/gql/queries/get-navigation-documents";
import getNavigationFormsQuery from "ember-caluma/gql/queries/get-navigation-forms";
import getDocumentAnswersQuery from "ember-caluma/gql/queries/get-document-answers";
import getDocumentFormsQuery from "ember-caluma/gql/queries/get-document-forms";
import { getOwner } from "@ember/application";
import { assert } from "@ember/debug";

const isDisplayableDocument = doc =>
doc &&
doc.visibleFields.length &&
!doc.visibleFields.every(field => field.questionType === "FormQuestion");

const buildParams = (section, subSections) => {
if (!section) {
return [];
}

return [
{ section: section.question.slug, subSection: undefined },
...subSections.map(s => ({
section: section.question.slug,
subSection: s.question.slug
}))
];
};

const buildTree = (rootDocument, documents, forms) => {
if (rootDocument.__typename === "Document") {
rootDocument.form = forms.find(
form => form.slug === rootDocument.form.slug
);
}

rootDocument.answers.edges.forEach(answer => {
if (answer.node.__typename === "FormAnswer") {
const childDocument = documents.find(
doc => doc.form.slug === answer.node.question.subForm.slug
);

assert(
`Document for form "${answer.node.question.subForm.slug}" not found`,
childDocument
);

answer.node.formValue = buildTree(childDocument, documents, forms);
}
});

return rootDocument;
};

/**
* Component to render a form with navigation.
*
Expand All @@ -77,15 +36,30 @@ const buildTree = (rootDocument, documents, forms) => {
* ```
*
* @class CfContentComponent
* @yield {Object} content
* @yield {Document} content.document
* @yield {CfFormComponent} content.form
* @yield {CfNavigationComponent} content.navigation
* @yield {CfPaginationComponent} content.pagination
*/
export default Component.extend(ComponentQueryManager, {
layout,

documentStore: service(),
router: service(),
calumaStore: service(),

init() {
this._super(...arguments);

assert(
"A `documentId` must be passed to `{{cf-content}}`",
this.documentId
);
},

/**
* The ID of the nested document to display the navigation for
* The uuid of the document to display
*
* @argument {String} documentId
*/
documentId: null,
Expand All @@ -99,27 +73,51 @@ export default Component.extend(ComponentQueryManager, {
context: null,

/**
* Form slug of currently visible section
* Whether the form renders in disabled state
*
* @argument {String} section
* @readonly
* @argument {Boolean} disabled
*/
section: reads("router.currentRoute.queryParams.section"),
disabled: false,

/**
* Form slug of currently visible sub-section
* The document to display
*
* @argument {String} subSection
* @readonly
* @property {Document} document
*/
subSection: reads("router.currentRoute.queryParams.subSection"),
document: reads("data.lastSuccessful.value"),

/**
* Whether the form renders in disabled state
*
* @argument {Boolean} disabled
*/
disabled: false,
navigation: computed("document", function() {
if (!this.document) return;

return this.calumaStore.push(
Navigation.create(getOwner(this).ownerInjection(), {
document: this.document
})
);
}),

fieldset: computed(
"document.{fieldsets.[],raw.form.slug}",
"router.currentRoute.queryParams.displayedForm",
function() {
if (!this.document) return;

const slug =
this.get("router.currentRoute.queryParams.displayedForm") ||
this.get("document.raw.form.slug");

const fieldset = this.document.fieldsets.find(
fieldset => fieldset.form.slug === slug
);

assert(
`The fieldset \`${slug}\` does not exist in this document`,
fieldset
);

return fieldset;
}
),

data: computed("documentId", function() {
const task = this.get("dataTask");
Expand All @@ -130,181 +128,30 @@ export default Component.extend(ComponentQueryManager, {
}),

dataTask: task(function*() {
if (!this.documentId) return null;

const rootId = window.btoa(`Document:${this.documentId}`);
if (!this.documentId) return;

const documents = (yield this.apollo.query(
const [answerDocument] = (yield this.apollo.query(
{
query: getNavigationDocumentsQuery,
variables: { rootDocument: rootId },
fetchPolicy: "network-only"
query: getDocumentAnswersQuery,
networkPolicy: "network-only",
variables: { id: this.documentId }
},
"allDocuments.edges"
)).map(({ node }) => node);

const forms = (yield this.apollo.query(
const [formDocument] = (yield this.apollo.query(
{
query: getNavigationFormsQuery,
variables: {
slugs: documents.map(doc => doc.form.slug).sort()
},
fetchPolicy: "cache-first"
query: getDocumentFormsQuery,
networkPolicy: "cache-first",
variables: { id: this.documentId }
},
"allForms.edges"
"allDocuments.edges"
)).map(({ node }) => node);

return this.documentStore.find(
buildTree(documents.find(doc => doc.id === rootId), documents, forms)
return this.calumaStore.push(
Document.create(getOwner(this).ownerInjection(), {
raw: parseDocument({ ...answerDocument, ...formDocument })
})
);
}),

rootDocument: reads("data.lastSuccessful.value"),

displayedDocument: computed(
"section",
"subSection",
"rootDocument",
function() {
try {
if (!this.get("rootDocument")) {
return null;
}
if (!this.get("section")) {
return this.get("rootDocument");
}
const section = this.get("rootDocument.fields").find(
field => field.question.slug === this.get("section")
);

if (!this.get("subSection")) {
return section.childDocument;
}
return section.childDocument.fields.find(
field => field.question.slug === this.get("subSection")
).childDocument;
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return null;
}
}
),

_sections: filterBy(
"rootDocument.visibleFields",
"visibleInNavigation",
true
),

_currentSection: computed("_sections.[]", "section", function() {
return this._sections.find(s => s.question.slug === this.section);
}),

_currentSubSection: computed(
"_currentSubSections.[]",
"subSection",
function() {
return this._currentSubSections.find(
s => s.question.slug === this.subSection
);
}
),

_currentSubSections: filterBy(
"_currentSection.childDocument.visibleFields",
"visibleInNavigation",
true
),

_currentSectionIndex: computed("_sections.[]", "section", function() {
return this._sections.indexOf(this._currentSection);
}),

_previousSection: computed("_currentSectionIndex", function() {
return this._currentSectionIndex > 0
? this._sections[this._currentSectionIndex - 1]
: null;
}),

_nextSection: computed(
"_currentSectionIndex",
"_sections.length",
function() {
return this._currentSectionIndex < this._sections.length
? this._sections[this._currentSectionIndex + 1]
: null;
}
),

_previousSubSections: filterBy(
"_previousSection.childDocument.visibleFields",
"visibleInNavigation",
true
),

_nextSubSections: filterBy(
"_nextSection.childDocument.visibleFields",
"visibleInNavigation",
true
),

adjacentSections: computed(
"_previousSubSections.[]",
"_currentSubSections.[]",
"_nextSubSections.[]",
function() {
return [
...buildParams(this._previousSection, this._previousSubSections),
...buildParams(this._currentSection, this._currentSubSections),
...buildParams(this._nextSection, this._nextSubSections)
];
}
),

sectionIndex: computed(
"adjacentSections.[]",
"section",
"subSection",
function() {
return this.adjacentSections.findIndex(
s => s.section === this.section && s.subSection === this.subSection
);
}
),

previousSection: computed("adjacentSections.[]", "sectionIndex", function() {
return this.sectionIndex > 0
? this.adjacentSections[this.sectionIndex - 1]
: null;
}),

nextSection: computed("adjacentSections.[]", "sectionIndex", function() {
return this.sectionIndex < this.adjacentSections.length
? this.adjacentSections[this.sectionIndex + 1]
: null;
}),

// eslint-disable-next-line ember/no-observers
_displayedDocumentChanged: observer(
"displayedDocument",
"nextSection",
function() {
if (isDisplayableDocument(this.displayedDocument) || !this.nextSection) {
return;
}

later(this, () => once(this, "_transitionToNextSection"));
}
),

_transitionToNextSection() {
if (isDisplayableDocument(this.displayedDocument) || !this.nextSection) {
return;
}

this.router.replaceWith({ queryParams: this.nextSection }).then(() => {
this.element.scrollIntoView(true);
});
}
})
});
3 changes: 1 addition & 2 deletions addon/components/cf-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,12 @@ import { task, timeout } from "ember-concurrency";
export default Component.extend({
layout,
classNames: ["uk-margin"],
classNameBindings: ["field.question.hidden:uk-hidden"],
classNameBindings: ["field.hidden:uk-hidden"],

/**
* Task to save a field. This will set the passed value to the answer and
* save the field to the API after a timeout off 500 milliseconds.
*
* @todo Validate the value
* @method save
* @param {String|Number|String[]} value
*/
Expand Down
Loading

0 comments on commit b602056

Please sign in to comment.