Survey Slate | Designer Tools

Create, edit, and connect questions for both simple and complex surveys using a variety of input types. Also check out the User Guide for Survey Slate Designer.

test credentials for demoEditor (note - these credentials were blocked after being exposed on GitHug, so they don't fully work for testing)

~~~js
{
  "accessKeyId": "AKIAQO7DBPIFDAUBK4SL",
  "secretAccessKey": "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"
}

~~~
const login = applyCredentials(
  manualCredentials
)
display(manualCredentials)

Designer UI

questionsNoLayout
///!!!!!!!!!!!!!!!!!!!!!!!!!
///!!! Check if this should self-execute
///!!!!!!!!!!!!!!!!!!!!!!!!!
const syncSurveyUiInputToSurveyUi = () => {
  console.log("syncSurveyUiInputToSurveyUi");
  //if (!_.isEqual(viewof surveyUi.value, viewof surveyUiInput.value)) {
  //!!! Note: Chaning due to different definition of viewof
  //if (!_.isEqual(surveyUi.value, surveyUiInput.value)) {
  if (!_.isEqual(surveyUiElement.value, surveyUiInputElement.value)) {
    console.log("syncSurveyUiInputToSurveyUi: change detected");
    //!!! Note: Chaning due to different definition of viewof
    //viewof surveyUi.value = viewof surveyUiInput.value;
    //surveyUi.value = surveyUiInput.value;
    surveyUiElement.value = surveyUiInput.value;
      // Manually updating the UI state
      // viewof surveyUi.applyValueUpdates();
    //viewof surveyUi.dispatchEvent(new Event('input', {bubbles: true}))
        //!!! Note: Chaning due to different definition of viewof
    //surveyUi.dispatchEvent(new Event('input', {bubbles: true}))
    surveyUiElement.dispatchEvent(new Event('input', {bubbles: true}))
  }
}
///!!!!!!!!!!!!!!!!!!!!!!!!!
///!!! Check if this should self-execute
///!!!!!!!!!!!!!!!!!!!!!!!!!

const syncSurveyOutput = () => {
  console.log("surveyOutput")
  // convert ui representation (pages -> cells) to {questions, layout, config} for storage.

  if (surveyUiOutput.pages.length === 0) return invalidation;
  // Extract questions
  const questions = new Map();
  surveyUiOutput.pages.forEach(page => {
    page.cells.forEach(cell => {
      questions.set(cell.id, uiCellToQuestion({
        ...cell.inner.result,
        type: cell.inner.type,
      }))
    })
  });

  // Extract layout
  const layout = [];
  surveyUiOutput.pages.forEach(page => {
    page.cells.forEach(cell => {
      const connections = cell?.connections?.connections || []
      const set = connections.map(c => c.set).join(",");
      layout.push({
        id: cell.id,
        menu: page.title,
        set,
        role: set === "" ? "" : connections.map(c => c.role).join(","),
      })
    })
  });

  // Extract config
  /// NOTE: Adjusted to account for different definition of viewof under Framework
  const config = {
  //  ...viewof surveyConfig.value, // carry over initial state
  //  ...surveyConfig.value, // carry over initial state
    ...surveyConfig, // carry over initial state
    pageTitle: surveyUiOutput.metadata.title
  };
  
  // viewof surveyOutput.value = {
  //surveyOutput.value = {
  surveyOutputElement.value = {
    questions,
    layout,
    config
  };
  //viewof surveyOutput.dispatchEvent(new Event('input', {bubbles: true}))
  //surveyOutput.dispatchEvent(new Event('input', {bubbles: true}))
  surveyOutputElement.dispatchEvent(new Event('input', {bubbles: true}))
};
display(syncSurveyOutput)

Autosave UI

Export

Persistence

SyntaxError: Unexpected token (7:9)
SyntaxError: Unexpected token (7:9)
const revertChanges = async function() {
  //const version = await files.load(viewof settings.value.versions.at(-1))
  const version = await files.load(settings.value.versions.at(-1))
  //viewof settings.value = {
    settings.value = {
    //...viewof settings.value,
    ...settings.value,
    configs: [
      //...(viewof settings.value.configs || []),
      ...(settings.value.configs || []),
      version.config
    ],
    questions: [
      //...(viewof settings.value.questions || []),
      ...(settings.value.questions || []),
      version.questions
    ],
    layout: [
      //...(viewof settings.value.layout || []),
      ...(settings.value.layout || []),
      version.layout
    ]
  };
  
  //await files.save("settings.json", viewof settings.value);
  await files.save("settings.json", settings.value);
  //viewof survey.dispatchEvent(new Event('input', {bubbles: true})) // reload everything
  survey.dispatchEvent(new Event('input', {bubbles: true})) // reload everything
}
//!!!!!!!!!!!!!!!!!
// NOTE: re-defining to work with Framework's Mutable 
//!!!!!!!!!!!!!!!!!
//const initialQuestionLoader = {
//  if (!initialLoadQuestions) {
//    mutable initialLoadQuestions = true;
//    viewof questions.value = await loadQuestions(settings.questions[settings.questions.length - 1])
//    viewof questions.dispatchEvent(new Event('input', {bubbles: true}));
//  }
//  return "Initial Question Loader"
//}


const initialQuestionLoader = async () => {
  if (!initialLoadQuestions) {
    // Mark that we've done the initial load
    setInitialLoadQuestions(true);

//NOTE: Adjusting for different definition of viewof
    // Programmatically set the questions input’s value
    questionsElement.value = await loadQuestions(
      settings.questions[settings.questions.length - 1]
    );

    // Notify the reactive runtime that the input changed
    questionsElement.dispatchEvent(new Event("input", {bubbles: true}));
  }

  display("Initial Question Loader");
};

Import external CSV of questions

//!!!!!!!!!!!!!!!!!
// NOTE: Need to re-work this as an async function due to await
//!!!!!!!!!!!!!!!!!
const onQuestionUpload = async () => {
  //viewof questions.value = csvToQuestions(await questionUpload.csv());
  //!!!! Experimental
  //questions.value = csvToQuestions(await questionUpload.csv());
  questionsElement.value = csvToQuestions(await questionUpload.csv());
  //viewof questions.dispatchEvent(new Event('input', {bubbles: true}));
    //!!!! Experimental
  //questions.dispatchEvent(new Event('input', {bubbles: true}));
  questionsElement.dispatchEvent(new Event('input', {bubbles: true}));
};
display(onQuestionUpload)
function csvToQuestions(csv) {
  return csv.reduce(
    (acc, row) => {
      

      // Now append the rows question attributes and values to the current question being processed
      const attribute = row['key'];
      const value = row['value'];
      const id = row['id'] || acc.previous?.id;

      let current = acc.previous;
      
      if (id != acc.previous?.id) {
        current = {
          id: id
        }
        acc.questions.push(current)
      }

      const arrays = ['options', 'rows', 'columns'];
      if (arrays.some(arr => attribute.startsWith(arr))) {
        // But if the element is packed as an array we don't unwind
        let packed = false;
        try {
          if (Array.isArray(eval(value))) {
            packed = true;
            current[attribute] = value;
          }
        } catch (err) {}

        if (attribute === 'rows' && !Number.isNaN(+value)) {
          // When rows is in a textarea is it not in an array
          current[attribute] = value;
        } else if (!packed) {
          // Arrays come in a list of elements
          const array = current[attribute] || [];
          if (arrays.includes(attribute)) {
            array.push({
              value: value,
              label: value
            });
          } else {
            array.push(value);
          }
  
          current[attribute] = array; 
        }
      } else {
        current[attribute] = sanitizeValue(value);
      }

      return {
        questions: acc.questions,
        previous: current
      }
    }
    , {
      questions: [],
      previous: null
    }
  ).questions.reduce( // Index by id
    (map, q) => {
      const {id, ...value} = q
      map.set(id, value)
      return map;
    },
    new Map() // Map remembers insertion order which is useful
  )
}

Export questions to CSV

const updateQuestionsCsvDataUriView = () => {
  //viewof questionsCsvDataUriView.value = questionsCsvDataUri /* sync questionsCsvDataUri changes to the view */
  //NOTE: Adjusting for different definition of viewof
  //questionsCsvDataUriView.value = questionsCsvDataUri /* sync
  questionsCsvDataUriViewElement.value = questionsCsvDataUri /* sync questionsCsvDataUri changes to the view */
  //viewof questionsCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
  //NOTE: Adjusting for different definition of viewof
  //questionsCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
  questionsCsvDataUriViewElement.dispatchEvent(new Event('input', {bubbles: true}))
}
SyntaxError: Unexpected token (1:55)
const downloadQuestionsCsv = htl.html`<a href=${viewof questionsCsvDataUriView.value} download="questions_${Date.now()}.csv">
  Download questions.csv
</a>
${exportQuestionsProblems.length > 0 ? md`<mark> Warning, some questions are not exporting properly, you may lose data in export` : null}
`
SyntaxError: Unexpected keyword 'const' (2:2)

Layout

Import layout from CSV

const onLayoutUpload = async() => {
  //viewof layoutData.value = {data: csvToLayout(await layoutUpload.csv())}
  // NOTE: Changing due to re-definition of viewof
// layoutData.value = {data: csvToLayout(await layoutUpload.csv())}
 layoutDataElement.value = {data: csvToLayout(await layoutUpload.csv())}
  //viewof layoutData.dispatchEvent(new Event('input', {bubbles: true}))
  // NOTE: Changing due to re-definition of viewof
//  layoutData.dispatchEvent(new Event('input', {bubbles: true}))
layoutDataElement.dispatchEvent(new Event('input', {bubbles: true}))
};
display(onLayoutUpload)

Export layout to CSV

const layoutCsvDataUri = URL.createObjectURL(new Blob([ d3.csvFormat(exportLayoutCSV) ], { type: 'text/csv' }));
//viewof layoutCsvDataUriView = Inputs.input(undefined)
const layoutCsvDataUriViewElement = Inputs.input(undefined);
const layoutCsvDataUriView = Generators.input(layoutCsvDataUriViewElement);
display(layoutCsvDataUriViewElement)
const updateLayoutCsvDataUriView = () => {
  //viewof layoutCsvDataUriView.value = layoutCsvDataUri
  // Note: Adjusting due to different definition of viewof
  //layoutCsvDataUriView.value = layoutCsvDataUri
    layoutCsvDataUriViewElement.value = layoutCsvDataUri
  //viewof layoutCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
  // Note: Adjusting due to different definition of viewof
  //layoutCsvDataUriView.dispatchEvent(new Event('input', {bubbles: true}))
layoutCsvDataUriViewElement.dispatchEvent(new Event('input', {bubbles: true}))
}
SyntaxError: Unexpected token (1:52)
const downloadLayoutCsv = htl.html`<a href=${viewof layoutCsvDataUriView.value} download="layout_${Date.now()}.csv">
  Download layout.csv
</a>
${exportLayoutProblems.length > 0 ? md`<mark> Warning, some layouts are not exporting properly, you may lose data in export` : null}
`
//!!!!!!!!!!!!!!!!!
//!!!!!!!!!!!!!!!!!
//!!!!!!!!!!!!!!!!!

//!!!! NOTE: This does not appear to have an appropriate setter.

//!!!!!!!!!!!!!!!!!
//!!!!!!!!!!!!!!!!!

//!!!!!!!!!!!!!!!!!
// NOTE: Adjusting this to define a Mutable and a setter function

const initialLoadLayout = Mutable(false)

//const initialLayoutLoader = {
//  if (!initialLoadLayout) {
//    mutable initialLoadLayout = true;
//    setLayout([...await loadLayout(settings.layout[settings.layout.length - 1])])
//  }
//  return "Initial Layout Loader"
//}
const initialLayoutLoader = async () => {
  if (!initialLoadLayout.value) {
    initialLoadLayout.value = true; // mark as loaded

    const latestLayoutKey = settings.layout[settings.layout.length - 1];
    const layout = await loadLayout(latestLayoutKey);
    setLayout([...layout]);
  }
  return "Initial Layout Loader";
};
function setLayout(data) {
  const choices = learnChoices(data);
  
  menuOptions.data = choices["menu"]
  setOptions.data = choices["set"]
  //viewof menuOptions.dispatchEvent(new Event('input', {bubbles: true}))
  menuOptions.dispatchEvent(new Event('input', {bubbles: true}))
  //viewof setOptions.dispatchEvent(new Event('input', {bubbles: true}))
  setOptions.dispatchEvent(new Event('input', {bubbles: true}))
  
  
  layoutData.data = data;
  //viewof layoutData.dispatchEvent(new Event('input', {bubbles: true}))
  layoutData.dispatchEvent(new Event('input', {bubbles: true}))
}
const learnChoices = (data) => {
  const columns = ["menu", "set"]
  const counts = data.reduce(
    (arr, l) => {
      columns.forEach(c => { 
        arr[c] = arr[c] || {};
        arr[c][l[c]] = (arr[c][l[c]] || 0) + 1 
      })
      return arr;
    }, {})
  return Object.fromEntries(Object.entries(counts).map(([key, counts]) => {
    return [key, Object.keys(counts).map(k => ({[key]: k}))]
  }))
}

Export UI

const sampleExportUi = exportUi()
SyntaxError: Unexpected token (6:21)
const exportUi = () => {
  const now = Date.now();

  return viewUI`<div class="space-y-3">
  <div>
    <a href=${viewof questionsCsvDataUriView.value} download="questions_${Date.now()}.csv">Download Questions</a>
  </div>

  <div>
    <a href=${viewof layoutCsvDataUriView.value} download="layout_${Date.now()}.csv">Download Layout</a>
  </div>
</div>`
}

Import UI

//viewof sampleImportUi = importUi()
const sampleImportUiElement = importUi();
const sampleImportUi = Generators.input(sampleImportUiElement);
display(sampleImportUiElement)

sampleImportUi
SyntaxError: Unexpected token (5:13)
const importUi = (afterSave) => {
  const submitFiles = async ()  => {
    if (ui.value.questionsCsv) {
      console.log('Updating questions CSV')
      viewof questions.value = csvToQuestions(await ui.value.questionsCsv.csv());
    }

    if (ui.value.layoutCsv) {
      console.log('Updating layout CSV')
      viewof layoutData.value = {data: csvToLayout(await ui.value.layoutCsv.csv())}
    }

    if (ui.value.questionsCsv || ui.value.layoutCsv) {
      viewof questions.dispatchEvent(new Event('input', {bubbles: true}));
      viewof layoutData.dispatchEvent(new Event('input', {bubbles: true}))
    }

    if (typeof afterSave === 'function') {
      afterSave();
    }
  }
  const submit = Inputs.button(buttonLabel({label: "Save"}), {reduce: submitFiles});
  
  const ui = viewUI`<div class="space-y-3">
  <h3 class="f5">Questions CSV file</h3>
  ${['questionsCsv', fileInput({prompt: "Drop questions as a CSV file here"})]}
  <h3 class="f5">Layout CSV file</h3>
  ${['layoutCsv', fileInput({prompt: "Drop layout as a CSV file here"})]}
  <div>
    ${submit}
  </div>`
  
  return ui;
}

Edit user choices within the layout Editors

function selection(title) {
  const choices = dataEditor([], {
    columns: [title],
    width: {
      [title]: "400px"
    },
    format: {
      [title]: (d) => Inputs.text({value: d, width: "400px", disabled: true}),
    },
  })
  const addForm = viewUI`<div style="display: flex;">
    ${[title, Inputs.text({
      label: `Add ${title}`
    })]}
    ${Inputs.button("add", {
      reduce: () => {
        choices.value.data = [...choices.value.data, addForm.value]
        addForm[title].value = '';
        choices.dispatchEvent(new Event('input', {bubbles: true}));
      }
    })}
  `

  return viewUI`<div><details>
      <summary>Edit choices for <b>${title}</b></summary>
      ${['...', choices]}${cautious(() => addForm)}
  `
}
//viewof menuOptions = selection("menu")
const menuOptionsElement = selection("menu");
const menuOptions = Generators.input(menuOptionsElement);
display(menuOptionsElement)

//viewof setOptions = selection("set")
const setOptionsElement = selection("set");
const setOptions = Generators.input(setOptionsElement);
display(setOptionsElement)

Layout Data\

SyntaxError: Unexpected token (1:7)
viewof layout = Inputs.input(layoutData)
/*
{ 
  const menuOptionsArr = menuOptions.data.map(d => d["menu"]);
  const setOptionsArr = setOptions.data.map(d => d["set"]);
  
  return dataEditor(layoutData.data, {
    columns: ["id", "menu", "set", "role"],
    format: {
      "id": (d) => Inputs.text({value: d}),
      "menu": (d) => Inputs.select(menuOptionsArr, {value: d}),
      "set": (d) => Inputs.select(setOptionsArr, {value: d}),
      "role": (d) => Inputs.text({value: d})
    },
    width: {
      "set": "100px"
    },
    tableClass: "layout",
    stylesheet: `
      .layout .col-cell_name form {
        width: 300px
      }
      .layout .col-menu form {
        width: 100px
      }
      .layout .col-set form {
        width: 100px
      }
    `
  })
}*/

Data Quality Checks

Questions that have no layout

SyntaxError: 'return' outside of function (4:4)
{
  const results = [...questionsNoLayout.entries()].map(([k, v]) => ({id: k, ...v}));
  if (results.length > 0) {
    return dataEditor(results, {
      columns: ["id", "type"]
    })
  } else {
    return md`✅ The are no questions with no layout`
  }
}
questionsNoLayout.values().next()

Layouts with no question\

SyntaxError: 'return' outside of function (4:4)
{
  const results = layoutsNoQuestion.map(([name, layoutArray]) => layoutArray[0]);
  if (results.length > 0) {
    return dataEditor(results)
  } else {
    return md`✅ The are no layouts with no questions`
  }
}

Questions with options but some of the options do not have a 'value'

All options need value's defined, this is the key used to ensure updates to question text do not affect the endusers pre-existing answers.

SyntaxError: 'return' outside of function (3:4)
{
  if (optionsWithoutValue.length > 0) {
    return md`⚠️ There are ${optionsWithoutValue.length} mistakes
  ${optionsWithoutValue.map(mistake => `\n- ${mistake[1].cell_name}`)}`
  } else {
    return md`✅ All options have a value defined`
  }
}
const optionsWithoutValue = [...surveyOutput.questions.entries()].map(([k, q]) => [k, reifyAttributes(q)]).filter(([name, question]) => question.options &&  !question.options.some(option => option.value))
const layoutById = d3.group(layout.data, d => d.id)
const duplicateLayouts = Object.fromEntries([...layoutById.entries()].filter(([name, layoutArr]) => layoutArr.length  > 1))
const questionsNoLayout = new Map([...questions.entries()].filter(([name, q]) => !layoutById.has(name)))
const layoutsNoQuestion = ([...layoutById.keys()].filter(name => !surveyOutput.questions.has(name))).map(k => [k, layoutById.get(k)])
const exportLayoutCSV = surveyOutput.layout
SyntaxError: Unexpected keyword 'const' (2:2)
const exportLayoutProblems  = {
  const exportedLayout = csvToLayout(exportLayoutCSV);
  
  const problems = [];
  for (var i = 0; i < Math.max(surveyOutput.layout.length, exportedLayout.length); i++) {
    if (!_.isEqual(layout.data[i], exportedLayout[i])) {
      problems.push({
        row: i,
        layout: layout.data[i],
        exportedLayout: exportedLayout[1]
      })
    }
  }
  return problems;
}

Config

Config is additional data that might be useful such as the menu display titles.

//viewof latestConfig = editor({
const latestConfigElement = editor({
  type: "object",
  title: "Config",
  properties: {
    pageTitle: {
      type: "string"
    },
    menuSegmentLabels: {
      type: "object",
      additionalProperties: { type: "string" }
    }
  }
}, {
  theme: "spectre",
  disable_edit_json: true,
  disable_properties: false,
  iconlib: "spectre",
  show_errors: "always",
  prompt_before_delete: "false"
});
const latestConfig = Generators.input(latestConfigElement);
display(latestConfigElement)
//!!!!!!!!!!!!!!!!!
// NOTE: Re-worked as an async function (no generator) due to await
//!!!!!!!!!!!!!!!!!
// NOTE: Original (Observable notebook style):
// viewof save_config = async function* () {
//   if (viewof surveyConfig.value && !_.isEqual(latestConfig, viewof surveyConfig.value)) {
//     yield md`Saving...`
//     viewof surveyConfig.value = latestConfig;
//     await saveConfig(latestConfig);
//     await files.save("settings.json", viewof settings.value);
//     yield md`saved`
//   } else {
//     yield md`no changes`
//   }
// }


//!! This is an experimental departure. Check.
const save_config = async () => {
  //if (viewof surveyConfig.value && !_.isEqual(latestConfig, viewof surveyConfig.value)) {
  // NOTE: Adjusting due to different definition of viewof
  // NOTE: Using surveyConfigElement instead of viewof surveyConfig
  if (surveyConfigElement.value && !_.isEqual(latestConfig, surveyConfigElement.value)) {
    //yield md`Saving...`
    // NOTE: No yield here; caller is responsible for showing "Saving..." state

    //viewof surveyConfig.value = latestConfig;
    // NOTE: Changing due to different definition of viewof
    surveyConfigElement.value = latestConfig;

    await saveConfig(latestConfig);

    //await files.save("settings.json", viewof settings.value);
    // NOTE: Changing due to different definition of viewof
    await files.save("settings.json", settings.value);

    //yield md`saved`
    // NOTE: Return status instead of yielding markdown
    return "saved";
  } else {
    //yield md`no changes`
    // NOTE: Return status instead of yielding markdown
    return "no changes";
  }
};
//viewof surveyConfig = Inputs.input()
const surveyConfigElement = Inputs.input();
const surveyConfig = Generators.input(surveyConfigElement);
display(surveyConfigElement)

const sync_ui = () => {
  //viewof latestConfig.value = surveyConfig
  // latestConfig.value = surveyConfig
  // !!!!!!!!!!!!!
   // NOTE: Check if this works
  latestConfigElement.value = surveyConfig
}
const load_config = async () => {
  //viewof surveyConfig.value = settings.configs?.length > 0 
  // Note: Adjusting due to different definition of config element
  //surveyConfig.value = settings.configs?.length > 0 
  surveyConfigElement.value = settings.configs?.length > 0 
                                ? await loadConfig(settings.configs[settings.configs.length - 1])
                                : {};
  //viewof surveyConfig.dispatchEvent(new Event('input', {bubbles: true}))
  // Note: Adjusting due to different definition of config element
  //surveyConfig.dispatchEvent(new Event('input', {bubbles: true}))
  surveyConfigElement.dispatchEvent(new Event('input', {bubbles: true}))
}
//import {editor} from "@a10k/hello-json-editor"
import {editor} from "/components/hello-json-editor.js"
display(editor)

Styles

SyntaxError: Assignment to external variable 'styles' (1:0)
styles = html`<style>
/* Survey Editor */

.survey-editor__import,
.survey-editor__export,
.survey-editor__editor {
  display: none;
}

[data-survey-editor-state="import"] .survey-editor__import,
[data-survey-editor-state="export"] .survey-editor__export,
[data-survey-editor-state="editor"] .survey-editor__editor {
  display: block;
}

/* Styles when displayed as a stand alone notebook */
[data-standalone-designer-notebook] .observablehq > h2 {
  padding-top: var(--spacing-medium); 
  border-top: 1px solid;
  border-color: #eee; /* .b--light-gray */
}
</style>`

Styles for the in notebook demo

<style>
  .survey-ui {
    overflow-y: auto;
    max-height: 600px;
    overscroll-behavior-y: contain;
  }
</style>
const surveyPreviewTitle = md`## Survey Preview`
//viewof responses = {
const responsesElement = (() => {
  addMenuBehaviour;
  const view = surveyView(
    surveyOutput.questions,
    surveyOutput.layout,
    surveyOutput.config,
    new Map(),
    {
      putFile: (name) => console.log("mock save " + name),
      getFile: (name) => console.log("mock get " + name)
    }
  );
  return view;
})();
const responses = Generators.input(responsesElement);
display(responsesElement)
``

responses
import {surveyView, addMenuBehaviour} from '@categorise/surveyslate-styling'

Preview Answers

responses

Revert changes

Rollback survey to last deployed version

//viewof reollbackButton = Inputs.button("revert", {
const reollbackButtonElement = Inputs.button("revert", {
  reduce: async () => {
    await revertChanges();
  }
});
const reollbackButton = Generators.input(reollbackButtonElement);
display(reollbackButtonElement)

const deployTitle = md`## Deploy Survey Version`

Last deployed: SyntaxError: invalid expression


Cloud Configuration

login
const me = getUser()
const myTags = listUserTags(me.UserName)
const REGION = 'us-east-2'
//import {listObjects, getObject, putObject, listUsers, createUser, deleteUser, getUser, listAccessKeys, createAccessKey, deleteAccessKey, viewof manualCredentials, viewof mfaCode, saveCreds, listUserTags, tagUser, untagUser, iam, s3, listGroups, listGroupsForUser, addUserToGroup, removeUserFromGroup} with {REGION as REGION} from '@tomlarkworthy/aws'
import {listObjects, getObject, putObject, listUsers, createUser, deleteUser, getUser, listAccessKeys, createAccessKey, deleteAccessKey, manualCredentials, manualCredentialsElement, mfaCode, saveCreds, saveCredsElement, listUserTags, tagUser, untagUser, iam, s3, listGroups, listGroupsForUser, addUserToGroup, removeUserFromGroup, applyCredentials} from '/components/aws.js'
md`---

## Helpers`
const sanitizeValue = (text) => {
  text = text.trim()
  if (text === "TRUE") return true;
  if (text === "FALSE") return false;
  return text
}

Dependencies

//import {createQuestion, reifyAttributes, bindLogic, setTypes, config} from '@categorise/survey-components'
import {createQuestion, reifyAttributes, bindLogic, setTypes, config} from '/components/survey-components.js'
//import {toc} from "@bryangingechen/toc"
import {toc} from "/components/toc.js"
//import {view, cautious} from '@tomlarkworthy/view'
import {viewUI, cautious} from '/components/view.js'
//import {fileInput} from "@tomlarkworthy/fileinput"
import {fileInput} from "/components/fileinput.js"
//import {dataEditor} from '@tomlarkworthy/dataeditor'
import {dataEditor} from '/components/dataeditor.js'
//import {pageHeader, pageFooter, buttonLabel} from "@categorise/common-components"
import {pageHeader, pageFooter, buttonLabel} from "/components/common-components.js"
//import {localStorageView} from '@tomlarkworthy/local-storage-view'
import {localStorageView} from '/components/local-storage-view.js';
display(localStorageView)

//import { substratum } from "@categorise/substratum"
//substratum({ invalidation })