diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1c5fe7e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+# DOI Prefill
+
+Allows users create Drupal nodes with data retrieved from Crossref.
+
+## Installation
+
+Install as
+[usual](https://www.drupal.org/docs/extending-drupal/installing-modules).
+
+## Configuration
+
+The form at `admin/config/system/doi_field_setting`s allows you to map the fields returned by Crossref to fields
+in your Islandora installation e.g. Crossref's `Contributors` field can be mapped to `field_contributor`, or `field_linked_agent`.
+
+You may optionally map the Crossref genre fields to your own taxonomy term e.g. `journal-article` to `Journal Article`
+
+## Usage
+
+After installing and enabling and configuring the module add a link to `doi-prefill/doi-prepopulate` in your [Shortcuts menu](admin/config/user-interface/shortcut/manage/default/customize)
+
+Navigate to the form where you can enter a DOI, a collection for your new node, and choose whether you'd like to be taken the node's edit form or be returned to the same page.
+
+
+
+## Maintainers
+Current maintainers:
+
+* [Robertson Library](https://library.upei.ca/)
+
+## License
+[GPLv3](http://www.gnu.org/licenses/gpl-3.0.txt)
diff --git a/doi_prefill.links.menu.yml b/doi_prefill.links.menu.yml
new file mode 100644
index 0000000..4c7ba8d
--- /dev/null
+++ b/doi_prefill.links.menu.yml
@@ -0,0 +1,6 @@
+doi_prefill.doi_settings:
+ title: DOI Prefill Settings
+ description: Configure DOI mappings
+ parent: system.admin_config_system
+ route_name: doi_prefill.doi_settings
+ weight: 10
diff --git a/src/Form/DOIFieldSettingsForm.php b/src/Form/DOIFieldSettingsForm.php
new file mode 100644
index 0000000..f00aaf9
--- /dev/null
+++ b/src/Form/DOIFieldSettingsForm.php
@@ -0,0 +1,324 @@
+entityTypeManager = $entityTypeManager;
+ $this->entityFieldManager = $entityFieldManager;
+ $this->utils = $utils;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container) {
+ return new static(
+ $container->get('entity_type.manager'),
+ $container->get('entity_field.manager'),
+ $container->get('islandora.utils'),
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getFormId(): string {
+ return 'doi_prefill_doi_field_settings';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getEditableConfigNames(): array {
+ return ['doi_prefill.settings'];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function buildForm(array $form, FormStateInterface $form_state): array {
+ $doi_fields = [
+ 'title' => 'Title',
+ 'contributors' => 'Contributors',
+ 'publisher' => 'Publisher',
+ 'doi' => 'DOI',
+ 'genre' => 'Genre',
+ 'issue' => 'Issue',
+ 'volume' => 'Volume',
+ 'date_issued' => 'Date issued',
+ 'abstract' => 'Abstract',
+ 'host_title' => 'Host title',
+ 'date_online' => 'Date online',
+ 'page_range' => 'Page range',
+ 'series_issn' => 'Series ISSN',
+ ];
+
+ $config = $this->config('doi_prefill.settings');
+ $fields = $this->entityFieldManager->getFieldDefinitions('node', 'islandora_object');
+ $field_options = ['title' => 'Title'];
+ foreach ($fields as $field) {
+ if ($field instanceof FieldConfig) {
+ $label = (string) $field->getLabel();
+ $name = $field->getName();
+ $field_options[$name] = $label;
+ }
+ }
+ asort($field_options);
+ $content_types = NodeType::loadMultiple();
+ $destination_content_types = [];
+ foreach ($content_types as $type) {
+ if ($this->utils->isIslandoraType('node', $type->id())) {
+ $destination_content_types[$type->id()] = $type->label();
+ }
+ }
+
+ $form['content_type'] = [
+ '#type' => 'select',
+ '#title' => $this->t('Content Type'),
+ '#description' => $this->t("Choose content type for new node"),
+ '#options' => $destination_content_types,
+ '#default_value' => $config->get('content_type'),
+ ];
+
+ // Display fields in a table format.
+ $form['description'] = [
+ '#type' => 'markup',
+ '#markup' => $this->t('DOI fields are returned by Crossref. Please choose field from your Islandora Installation to hold the returned value.'),
+ ];
+
+ $form['field_table'] = [
+ '#type' => 'table',
+ '#header' => [
+ $this->t('DOI Field'),
+ $this->t('Islandora Field'),
+ ],
+ '#prefix' => '
',
+ '#suffix' => '
',
+ ];
+ foreach ($doi_fields as $machine_name => $field) {
+ $form['field_table'][$machine_name]['field_name'] = [
+ '#plain_text' => $field,
+ ];
+
+ // Add a dropdown for each field.
+ $form['field_table'][$machine_name]['dropdown'] = [
+ '#type' => 'select',
+ '#options' => $field_options,
+ '#default_value' => $config->get('field_settings')[$machine_name] ?? '',
+ ];
+ }
+ $form['#attached']['library'][] = 'doi_prefill/doi_field_selector_styles';
+
+ $form['pairs_description'] = [
+ '#type' => 'markup',
+ '#markup' => $this->t('DOI genre terms are returned by Crossref. Please choose any term you would like to replace from your own taxonomy.'),
+ ];
+
+ $doi_term_islandora_term_pairs = $form_state->get('doi_term_islandora_term_pairs');
+ if (!$doi_term_islandora_term_pairs) {
+ $doi_term_islandora_term_pairs = $config->get('doi_term_islandora_term_pairs');
+ }
+ if (empty($doi_term_islandora_term_pairs)) {
+ // Initialize as an empty array if no pairs exist.
+ $doi_term_islandora_term_pairs = [];
+ }
+
+ // Set the form state for entry_count and doi_term_islandora_term_pairs.
+ $form_state->set('doi_term_islandora_term_pairs', $doi_term_islandora_term_pairs);
+ $entry_count = count($doi_term_islandora_term_pairs);
+ $form_state->set('entry_count', $entry_count);
+
+ // Define the table structure for key-value pairs.
+ $form['doi_term_islandora_term_pairs'] = [
+ '#type' => 'table',
+ '#prefix' => '',
+ '#suffix' => '
',
+ ];
+
+ // Generate the table rows dynamically based on stored pairs.
+ foreach ($doi_term_islandora_term_pairs as $entry_id => $pair) {
+ $pair['entry_id'] = $entry_id;
+ $unique_id = $pair['entry_id'] ?? uniqid();
+ $form['doi_term_islandora_term_pairs'][$unique_id]['key'] = [
+ '#type' => 'textfield',
+ '#default_value' => $pair['key'] ?? '',
+ '#title' => $this->t('term from DOI'),
+ '#required' => TRUE,
+ ];
+
+ $form['doi_term_islandora_term_pairs'][$unique_id]['value'] = [
+ '#type' => 'textfield',
+ '#default_value' => $pair['value'] ?? '',
+ '#title' => $this->t('Genre term'),
+ '#required' => TRUE,
+ ];
+
+ // Hidden field to store the correct entry ID.
+ $form['doi_term_islandora_term_pairs'][$unique_id]['entry_id'] = [
+ '#type' => 'hidden',
+ '#value' => $entry_id,
+ ];
+
+ // Remove button for each entry.
+ $form['doi_term_islandora_term_pairs'][$unique_id]['remove'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Remove'),
+ '#submit' => ['::removeCallback'],
+ '#limit_validation_errors' => [],
+ '#ajax' => [
+ 'callback' => '::ajaxCallback',
+ 'wrapper' => 'key-value-pairs-wrapper',
+ ],
+ // Instead of relying on `#attributes`, set a unique `#name`!
+ '#name' => 'remove_' . $unique_id,
+ ];
+
+ }
+
+ // Button to add another key-value pair.
+ $form['add_more'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Add term'),
+ '#submit' => ['::addMoreCallback'],
+ '#ajax' => [
+ 'callback' => '::ajaxCallback',
+ 'wrapper' => 'key-value-pairs-wrapper',
+ ],
+ ];
+
+ // Submit button for the form.
+ $form['submit'] = [
+ '#type' => 'submit',
+ '#value' => $this->t('Save term mappings'),
+ ];
+
+ return parent::buildForm($form, $form_state);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function submitForm(array &$form, FormStateInterface $form_state): void {
+ if (!empty($form_state->getValue('field_table'))) {
+ foreach ($form_state->getValue('field_table') as $doi_field => $islandora_field) {
+ $field_settings[$doi_field] = $islandora_field['dropdown'];
+ }
+ }
+ $doi_term_islandora_term_pairs = $form_state->getValue('doi_term_islandora_term_pairs');
+ $content_type = $form_state->getValue('content_type');
+ $this->config('doi_prefill.settings')
+ ->set('field_settings', $field_settings)
+ ->set('doi_term_islandora_term_pairs', $doi_term_islandora_term_pairs)
+ ->set('content_type', $content_type)
+ ->save();
+ parent::submitForm($form, $form_state);
+ }
+
+ /**
+ * AJAX callback to refresh the form.
+ */
+ public function ajaxCallback(array &$form, FormStateInterface $form_state) {
+ return $form['doi_term_islandora_term_pairs'];
+ }
+
+ /**
+ * Adds more key-value pair fields.
+ */
+ public function addMoreCallback(array &$form, FormStateInterface $form_state) {
+ // Retrieve existing key-value pairs.
+ $doi_term_islandora_term_pairs = $form_state->get('doi_term_islandora_term_pairs') ?? [];
+
+ // Get submitted values and merge with stored ones.
+ $user_input = $form_state->getUserInput();
+ if (!empty($user_input['doi_term_islandora_term_pairs'])) {
+ foreach ($user_input['doi_term_islandora_term_pairs'] as $id => $values) {
+ if (!isset($doi_term_islandora_term_pairs[$id])) {
+ $doi_term_islandora_term_pairs[$id] = $values;
+ }
+ }
+ }
+
+ // Add a new empty entry with a unique ID.
+ $unique_id = uniqid();
+ $doi_term_islandora_term_pairs[$unique_id] = [
+ 'key' => '',
+ 'value' => '',
+ 'unique_id' => $unique_id,
+ ];
+
+ // Store the updated values.
+ $form_state->set('doi_term_islandora_term_pairs', $doi_term_islandora_term_pairs);
+ $form_state->set('entry_count', count($doi_term_islandora_term_pairs));
+
+ // Rebuild the form.
+ $form_state->setRebuild();
+ }
+
+ /**
+ * Remove chosen element.
+ */
+ public function removeCallback(array &$form, FormStateInterface $form_state) {
+ $triggering_element = $form_state->getTriggeringElement();
+ // Using hidden field to get around Drupal's issues with ajax.
+ $button_name = $triggering_element['#name'] ?? '';
+
+ // Extract the unique ID from the button name.
+ if (preg_match('/remove_(.+)/', $button_name, $matches)) {
+ $clicked_id = $matches[1];
+ }
+ $doi_term_islandora_term_pairs = $form_state->get('doi_term_islandora_term_pairs') ?? [];
+
+ if (isset($doi_term_islandora_term_pairs[$clicked_id])) {
+ unset($doi_term_islandora_term_pairs[$clicked_id]);
+ }
+ $form_state->set('doi_term_islandora_term_pairs', $doi_term_islandora_term_pairs);
+ $form_state->set('entry_count', count($doi_term_islandora_term_pairs));
+ $form_state->setRebuild();
+ }
+
+}