From 853d794bdd291f2e39e084749371753803033117 Mon Sep 17 00:00:00 2001
From: Marc Hanisch <mhanisch@gfz-potsdam.de>
Date: Tue, 29 Jun 2021 06:54:56 +0000
Subject: [PATCH] Firmware update actions

---
 components/CommonActionForm.vue               |   17 +-
 components/GenericActionForm.vue              |   45 +-
 components/SoftwareUpdateActionCard.vue       |  182 +++
 components/SoftwareUpdateActionForm.vue       |  277 ++++
 modelUtils/Compareables.ts                    |    2 +-
 models/ActionCommonDetails.ts                 |   95 ++
 models/GenericAction.ts                       |   48 +-
 models/SoftwareType.ts                        |  100 ++
 models/SoftwareUpdateAction.ts                |  131 ++
 pages/configurations/_id.vue                  |    4 +-
 pages/devices/_deviceId/actions.vue           |  344 ++---
 .../_actionId/edit.vue                        |   63 +-
 pages/devices/_deviceId/actions/new.vue       |   81 +-
 .../_actionId/edit.vue                        |  175 +++
 .../jsonapi/ActionAttachmentSerializer.ts     |  199 +++
 serializers/jsonapi/AttachmentSerializer.ts   |    8 +-
 .../jsonapi/DeviceAttachmentSerializer.ts     |    4 +-
 .../GenericActionAttachmentSerializer.ts      |  142 +-
 .../jsonapi/GenericActionSerializer.ts        |   41 +-
 .../jsonapi/PlatformAttachmentSerializer.ts   |    4 +-
 .../jsonapi/SoftwareTypeSerializer.ts         |   24 +-
 ...oftwareUpdateActionAttachmentSerializer.ts |   93 ++
 .../jsonapi/SoftwareUpdateActionSerializer.ts |  298 ++++
 services/Api.ts                               |   57 +-
 services/cv/SoftwareTypeApi.ts                |  155 +++
 ...ttachmentApi.ts => ActionAttachmentApi.ts} |   14 +-
 services/sms/DeviceApi.ts                     |   16 +
 services/sms/DeviceSoftwareUpdateActionApi.ts |  146 ++
 services/sms/GenericActionAttachmentApi.ts    |   62 +
 services/sms/GenericDeviceActionApi.ts        |    4 +-
 .../sms/PlatformSoftwareUpdateActionApi.ts    |  146 ++
 .../sms/SoftwareUpdateActionAttachmentApi.ts  |   62 +
 test/models/GenericAction.test.ts             |    1 +
 test/models/SoftwareUpdateAction.test.ts      |   76 +
 .../GenericActionAttachmentSerializer.test.ts |   16 +-
 .../jsonapi/GenericActionSerializer.test.ts   |   10 +-
 .../jsonapi/SoftwareTypeSerializer.test.ts    |  104 ++
 ...reUpdateActionAttachmentSerializer.test.ts |  313 +++++
 .../SoftwareUpdateActionSerializer.test.ts    | 1229 +++++++++++++++++
 test/utils/urlHelper.test.ts                  |   16 +-
 utils/urlHelpers.ts                           |   18 +
 41 files changed, 4349 insertions(+), 473 deletions(-)
 create mode 100644 components/SoftwareUpdateActionCard.vue
 create mode 100644 components/SoftwareUpdateActionForm.vue
 create mode 100644 models/ActionCommonDetails.ts
 create mode 100644 models/SoftwareType.ts
 create mode 100644 models/SoftwareUpdateAction.ts
 rename pages/devices/_deviceId/actions/{ => generic-device-actions}/_actionId/edit.vue (77%)
 create mode 100644 pages/devices/_deviceId/actions/software-update-actions/_actionId/edit.vue
 create mode 100644 serializers/jsonapi/ActionAttachmentSerializer.ts
 rename models/Action.ts => serializers/jsonapi/SoftwareTypeSerializer.ts (64%)
 create mode 100644 serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.ts
 create mode 100644 serializers/jsonapi/SoftwareUpdateActionSerializer.ts
 create mode 100644 services/cv/SoftwareTypeApi.ts
 rename services/sms/{GenericDeviceActionAttachmentApi.ts => ActionAttachmentApi.ts} (81%)
 create mode 100644 services/sms/DeviceSoftwareUpdateActionApi.ts
 create mode 100644 services/sms/GenericActionAttachmentApi.ts
 create mode 100644 services/sms/PlatformSoftwareUpdateActionApi.ts
 create mode 100644 services/sms/SoftwareUpdateActionAttachmentApi.ts
 create mode 100644 test/models/SoftwareUpdateAction.test.ts
 create mode 100644 test/serializers/jsonapi/SoftwareTypeSerializer.test.ts
 create mode 100644 test/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.test.ts
 create mode 100644 test/serializers/jsonapi/SoftwareUpdateActionSerializer.test.ts

diff --git a/components/CommonActionForm.vue b/components/CommonActionForm.vue
index 8d7663479..c8af422a0 100644
--- a/components/CommonActionForm.vue
+++ b/components/CommonActionForm.vue
@@ -29,7 +29,7 @@ implied. See the Licence for the specific language governing
 permissions and limitations under the Licence.
 -->
 <template>
-  <v-container>
+  <div>
     <v-row>
       <v-col cols="12" md="12">
         <v-textarea
@@ -82,7 +82,7 @@ permissions and limitations under the Licence.
         />
       </v-col>
     </v-row>
-  </v-container>
+  </div>
 </template>
 
 <script lang="ts">
@@ -94,7 +94,7 @@ import { Component, Prop, Vue } from 'nuxt-property-decorator'
 
 import { Attachment } from '@/models/Attachment'
 import { Contact } from '@/models/Contact'
-import { GenericAction } from '@/models/GenericAction'
+import { IActionCommonDetails, ActionCommonDetails } from '@/models/ActionCommonDetails'
 
 /**
  * A class component for a set of common form fields for actions
@@ -108,15 +108,14 @@ export default class CommonActionForm extends Vue {
   private contactIsValid = true
 
   /**
-   * a GenericAction
+   * an IActionCommonDetails like object
    */
   @Prop({
-    default: new GenericAction(),
     required: true,
     type: Object
   })
   // @ts-ignore
-  readonly value!: GenericAction
+  readonly value!: IActionCommonDetails
 
   /**
    * a list of available attachments
@@ -159,7 +158,7 @@ export default class CommonActionForm extends Vue {
    * @fires CommonActionForm#input
    */
   set description (value: string) {
-    const actionCopy = GenericAction.createFromObject(this.value)
+    const actionCopy = ActionCommonDetails.createFromObject(this.value)
     actionCopy.description = value
     /**
      * descriptionChange event
@@ -180,7 +179,7 @@ export default class CommonActionForm extends Vue {
    * @fires CommonActionForm#input
    */
   set contact (value: Contact | null) {
-    const actionCopy = GenericAction.createFromObject(this.value)
+    const actionCopy = ActionCommonDetails.createFromObject(this.value)
     actionCopy.contact = value || null
     /**
      * contactChange event
@@ -201,7 +200,7 @@ export default class CommonActionForm extends Vue {
    * @fires CommonActionForm#input
    */
   set actionAttachments (value: Attachment[]) {
-    const actionCopy = GenericAction.createFromObject(this.value)
+    const actionCopy = ActionCommonDetails.createFromObject(this.value)
     actionCopy.attachments = value
     /**
      * attachments event
diff --git a/components/GenericActionForm.vue b/components/GenericActionForm.vue
index 986dc3f11..6571e76da 100644
--- a/components/GenericActionForm.vue
+++ b/components/GenericActionForm.vue
@@ -30,7 +30,7 @@ implied. See the Licence for the specific language governing
 permissions and limitations under the Licence.
 -->
 <template>
-  <v-container>
+  <div>
     <v-form
       ref="datesForm"
       v-model="datesAreValid"
@@ -38,7 +38,7 @@ permissions and limitations under the Licence.
     >
       <v-row>
         <v-col cols="12" md="6">
-          <date-time-picker
+          <DateTimePicker
             :value="actionCopy.beginDate"
             label="Start date"
             placeholder="e.g. 2000-01-31 12:00"
@@ -47,7 +47,7 @@ permissions and limitations under the Licence.
           />
         </v-col>
         <v-col cols="12" md="6">
-          <date-time-picker
+          <DateTimePicker
             :value="actionCopy.endDate"
             label="End date"
             placeholder="e.g. 2001-01-31 12:00"
@@ -59,11 +59,12 @@ permissions and limitations under the Licence.
     </v-form>
     <CommonActionForm
       ref="commonForm"
-      v-model="action"
+      :value="actionCopy"
       :attachments="attachments"
       :rules="[rules.contactNotNull]"
+      @input="updateCommonFields"
     />
-  </v-container>
+  </div>
 </template>
 
 <script lang="ts">
@@ -77,6 +78,7 @@ import { DateTime } from 'luxon'
 
 import { Attachment } from '@/models/Attachment'
 import { GenericAction } from '@/models/GenericAction'
+import { ActionCommonDetails } from '@/models/ActionCommonDetails'
 
 import CommonActionForm from '@/components/CommonActionForm.vue'
 import DateTimePicker from '@/components/DateTimePicker.vue'
@@ -87,8 +89,8 @@ import DateTimePicker from '@/components/DateTimePicker.vue'
  */
 @Component({
   components: {
-    DateTimePicker,
-    CommonActionForm
+    CommonActionForm,
+    DateTimePicker
   }
 })
 // @ts-ignore
@@ -129,14 +131,6 @@ export default class GenericActionForm extends Vue {
     this.createActionCopy(this.value)
   }
 
-  get action (): GenericAction {
-    return this.actionCopy
-  }
-
-  set action (value: GenericAction) {
-    this.$emit('input', value)
-  }
-
   /**
    * sets the start date and validates start- and enddate
    *
@@ -163,6 +157,13 @@ export default class GenericActionForm extends Vue {
     this.$emit('input', this.actionCopy)
   }
 
+  updateCommonFields (action: ActionCommonDetails) {
+    this.actionCopy.description = action.description
+    this.actionCopy.contact = action.contact
+    this.actionCopy.attachments = action.attachments.map((a: Attachment) => Attachment.createFromObject(a))
+    this.$emit('input', this.actionCopy)
+  }
+
   /**
    * validates the form based on its rules
    *
@@ -174,13 +175,10 @@ export default class GenericActionForm extends Vue {
   /**
    * a rule to validate the start date
    *
-   * @param {string} v - a string representation of the date as supplied by the datepicker component
    * @return {boolean | string} whether the date is valid or an error message
    */
-  validateInputForStartDate (v: string): boolean | string {
-    // NOTE: as the internals of the DatePicker component work with strings,
-    // the validation functions should expect strings, too
-    if (v === null || v === '') {
+  validateInputForStartDate (): boolean | string {
+    if (!this.actionCopy.beginDate) {
       return true
     }
     if (!this.actionCopy.endDate) {
@@ -195,13 +193,10 @@ export default class GenericActionForm extends Vue {
   /**
    * a rule to validate the end date
    *
-   * @param {string} v - a string representation of the date as supplied by the datepicker component
    * @return {boolean | string} whether the date is valid or an error message
    */
-  validateInputForEndDate (v: string): boolean | string {
-    // NOTE: as the internals of the DatePicker component work with strings,
-    // the validation functions should expect strings, too
-    if (v === null || v === '') {
+  validateInputForEndDate (): boolean | string {
+    if (!this.actionCopy.endDate) {
       return true
     }
     if (!this.actionCopy.beginDate) {
diff --git a/components/SoftwareUpdateActionCard.vue b/components/SoftwareUpdateActionCard.vue
new file mode 100644
index 000000000..b3c0addb5
--- /dev/null
+++ b/components/SoftwareUpdateActionCard.vue
@@ -0,0 +1,182 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020, 2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <v-card>
+    <v-card-subtitle class="pb-0">
+      <v-row no-gutters>
+        <v-col>
+          {{ value.updateDate | toUtcDate }}
+        </v-col>
+        <v-col
+          align-self="end"
+          class="text-right"
+        >
+          <slot name="menu" />
+        </v-col>
+      </v-row>
+    </v-card-subtitle>
+    <v-card-title class="pt-0">
+      {{ updateName }}
+    </v-card-title>
+    <v-card-subtitle class="pb-1">
+      <v-row
+        no-gutters
+      >
+        <v-col>
+          {{ value.contact.toString() }}
+        </v-col>
+        <v-col
+          align-self="end"
+          class="text-right"
+        >
+          <slot name="actions" />
+          <v-btn
+            icon
+            @click.stop.prevent="toggleVisibility()"
+          >
+            <v-icon>{{ isVisible() ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
+          </v-btn>
+        </v-col>
+      </v-row>
+    </v-card-subtitle>
+    <v-expand-transition>
+      <div
+        v-show="isVisible(value.id)"
+      >
+        <v-card-text
+          class="grey lighten-5 text--primary pt-2"
+        >
+          <v-row dense>
+            <v-col cols="12" md="4">
+              <label>
+                Version
+              </label>
+              {{ value.version }}
+            </v-col>
+            <v-col cols="12" md="4">
+              <label>
+                Repository
+              </label>
+              <!-- eslint-disable-next-line vue/no-v-html -->
+              <span v-html="repositoryLink" />
+            </v-col>
+          </v-row>
+          <label>Description</label>
+          {{ value.description }}
+        </v-card-text>
+      </div>
+    </v-expand-transition>
+  </v-card>
+</template>
+
+<script lang="ts">
+/**
+ * @file provides a component for a Software Update Action card
+ * @author <marc.hanisch@gfz-potsdam.de>
+ */
+import { Component, Prop, Vue } from 'nuxt-property-decorator'
+
+import { dateToDateTimeString } from '@/utils/dateHelper'
+import { protocolsInUrl } from '@/utils/urlHelpers'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
+
+/**
+ * A class component for Software Update Action card
+ * @extends Vue
+ */
+@Component({
+  filters: {
+    toUtcDate: dateToDateTimeString
+  }
+})
+// @ts-ignore
+export default class SoftwareUpdateActionCard extends Vue {
+  private showDetails: boolean = false
+
+  /**
+   * a SoftwareUpdateAction
+   */
+  @Prop({
+    default: () => new SoftwareUpdateAction(),
+    required: true,
+    type: Object
+  })
+  // @ts-ignore
+  readonly value!: SoftwareUpdateAction
+
+  /**
+   * whether the card expansion is shown or not
+   *
+   * @return {boolean} whether the card expansion is shown or not
+   */
+  isVisible (): boolean {
+    return this.showDetails
+  }
+
+  /**
+   * toggles the shown state of the card expansion
+   *
+   */
+  toggleVisibility (): void {
+    this.showDetails = !this.showDetails
+  }
+
+  /**
+   * returns an URL as an link
+   *
+   * All characters except 0-9, a-z, :, / and . are removed from the link to
+   * prevent xss attacks. If the URL doesn't start with a known protocol, it
+   * won't be wrapped.
+   *
+   * @return {string} the url wrapped in an HTML link element
+   */
+  get repositoryLink (): string {
+    // eslint-disable-next-line no-useless-escape
+    const url = this.value.repositoryUrl.replace(/[^a-zA-Z0-9:\/.-]/g, '')
+    if (protocolsInUrl(['https', 'http', 'ftp', 'ftps', 'sftp', 'dav', 'davs'], url)) {
+      return '<a href="' + url + '" target="_blank">' + url + '</a>'
+    }
+    return url
+  }
+
+  /**
+   * returns the name of the update
+   *
+   * @returns {string} the update name
+   */
+  get updateName (): string {
+    if (this.value.softwareTypeName.toLowerCase() === 'others') {
+      return 'Device Software Update'
+    }
+    return this.value.softwareTypeName + ' Update'
+  }
+}
+</script>
diff --git a/components/SoftwareUpdateActionForm.vue b/components/SoftwareUpdateActionForm.vue
new file mode 100644
index 000000000..e2f5ac913
--- /dev/null
+++ b/components/SoftwareUpdateActionForm.vue
@@ -0,0 +1,277 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020, 2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <div>
+    <v-row>
+      <v-col cols="12" md="6">
+        <v-form
+          ref="datesForm"
+          v-model="datesAreValid"
+          @submit.prevent
+        >
+          <DateTimePicker
+            :value="actionCopy.updateDate"
+            label="Update date"
+            :rules="[rules.dateNotNull]"
+            @input="setUpdateDateAndValidate"
+          />
+        </v-form>
+      </v-col>
+      <v-col cols="12" md="6">
+        <v-form
+          ref="softwareTypeForm"
+          @submit.prevent
+        >
+          <v-select
+            :value="actionCopy.softwareTypeUrl"
+            :items="softwareTypes"
+            clearable
+            :item-text="(x) => x.name"
+            :item-value="(x) => x.uri"
+            label="Software type"
+            :rules="[rules.softwareTypeNotNull]"
+            @input="setSoftwareType"
+          />
+        </v-form>
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col cols="12" md="6">
+        <v-text-field
+          :value="actionCopy.version"
+          label="Version"
+          placeholder="1.2.3"
+          @input="setVersion"
+        />
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col cols="12">
+        <v-form
+          ref="repositoryUrlForm"
+          @submit.prevent
+        >
+          <v-text-field
+            :value="actionCopy.repositoryUrl"
+            label="Repository URL"
+            placeholder="https://github.com/"
+            :rules="[rules.isUrl]"
+            @input="setRepositoryUrl"
+          />
+        </v-form>
+      </v-col>
+    </v-row>
+    <CommonActionForm
+      ref="commonForm"
+      :value="actionCopy"
+      :attachments="attachments"
+      :rules="[rules.contactNotNull]"
+      @input="updateCommonFields"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+/**
+ * @file provides a component for a Software Update Action form
+ * @author <marc.hanisch@gfz-potsdam.de>
+ */
+import { Component, Prop, Vue, Watch } from 'nuxt-property-decorator'
+
+import { DateTime } from 'luxon'
+
+import { Attachment } from '@/models/Attachment'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
+import { SoftwareType } from '@/models/SoftwareType'
+import { ActionCommonDetails } from '@/models/ActionCommonDetails'
+
+import { protocolsInUrl } from '@/utils/urlHelpers'
+
+import CommonActionForm from '@/components/CommonActionForm.vue'
+import DateTimePicker from '@/components/DateTimePicker.vue'
+
+/**
+ * A class component for a form for Generic Device Actions
+ * @extends Vue
+ */
+@Component({
+  components: {
+    CommonActionForm,
+    DateTimePicker
+  }
+})
+// @ts-ignore
+export default class SoftwareUpdateActionForm extends Vue {
+  private actionCopy: SoftwareUpdateAction = new SoftwareUpdateAction()
+  private datesAreValid: boolean = true
+  private rules: Object = {
+    dateNotNull: this.mustBeProvided('Update date'),
+    contactNotNull: this.mustBeProvided('Contact'),
+    softwareTypeNotNull: this.mustBeProvided('Software type'),
+    isUrl: this.isUrl
+  }
+
+  private softwareTypes: SoftwareType[] = []
+
+  /**
+   * a SoftwareUpdateAction
+   */
+  @Prop({
+    default: () => new SoftwareUpdateAction(),
+    required: true,
+    type: Object
+  })
+  // @ts-ignore
+  readonly value!: SoftwareUpdateAction
+
+  /**
+   * a list of available attachments
+   */
+  @Prop({
+    default: () => [],
+    required: false,
+    type: Array
+  })
+  // @ts-ignore
+  readonly attachments!: Attachment[]
+
+  async fetch (): Promise<any> {
+    await this.fetchSoftwareTypes()
+  }
+
+  async fetchSoftwareTypes (): Promise<any> {
+    this.softwareTypes = await this.$api.softwareTypes.findAllPaginated()
+  }
+
+  created () {
+    // create a copy of the original value on which all operations will be applied
+    this.createActionCopy(this.value)
+  }
+
+  /**
+   * sets the update date and validates
+   *
+   * @param {DateTime | null} aDate - the update date
+   */
+  setUpdateDateAndValidate (aDate: DateTime | null) {
+    this.actionCopy.updateDate = aDate
+    this.checkValidationOfDates()
+    this.$emit('input', this.actionCopy)
+  }
+
+  setSoftwareType (anUri: string | null) {
+    let aSoftwareType: SoftwareType | undefined
+    if (anUri) {
+      aSoftwareType = this.softwareTypes.find(i => i.uri === anUri)
+    }
+    this.actionCopy.softwareTypeUrl = aSoftwareType?.uri || ''
+    this.actionCopy.softwareTypeName = aSoftwareType?.name || ''
+    this.$emit('input', this.actionCopy)
+  }
+
+  setVersion (version: string) {
+    this.actionCopy.version = version
+    this.$emit('input', this.actionCopy)
+  }
+
+  setRepositoryUrl (repositoryUrl: string) {
+    this.actionCopy.repositoryUrl = repositoryUrl
+    this.$emit('input', this.actionCopy)
+  }
+
+  updateCommonFields (action: ActionCommonDetails) {
+    this.actionCopy.description = action.description
+    this.actionCopy.contact = action.contact
+    this.actionCopy.attachments = action.attachments.map((a: Attachment) => Attachment.createFromObject(a))
+    this.$emit('input', this.actionCopy)
+  }
+
+  /**
+   * validates the form based on its rules
+   *
+   */
+  checkValidationOfDates () {
+    return (this.$refs.datesForm as Vue & { validate: () => boolean }).validate()
+  }
+
+  /**
+   * a rule to check that an field is non-empty
+   *
+   * @param {string} fieldname - the (human readable) label of the field
+   * @return {(v: any) => boolean | string} a function that checks whether the field is valid or an error message
+   */
+  mustBeProvided (fieldname: string): (v: any) => boolean | string {
+    const innerFunc: (v: any) => boolean | string = function (v: any) {
+      if (v == null || v === '') {
+        return fieldname + ' must be provided'
+      }
+      return true
+    }
+    return innerFunc
+  }
+
+  /**
+   * checks whether the link is an valid URL or not
+   *
+   * to be honest, it just checks whether the string starts with http(s):// or similar
+   *
+   * @param {string} link - the link to validate
+   * @returns {boolean | string} true when valid, otherwise an error message
+   */
+  isUrl (link: string): boolean | string {
+    if (!protocolsInUrl(['http', 'https', 'ftp', 'ftps', 'sftp', 'ssh', 'dav', 'davs'], link)) {
+      return 'The link is not a valid URL.'
+    }
+    return true
+  }
+
+  /**
+   * checks if the form is valid
+   *
+   */
+  isValid (): boolean {
+    return this.checkValidationOfDates() &&
+      (this.$refs.commonForm as Vue & { isValid: () => boolean }).isValid() &&
+      (this.$refs.softwareTypeForm as Vue & { validate: () => boolean }).validate() &&
+      (this.$refs.repositoryUrlForm as Vue & { validate: () => boolean }).validate()
+  }
+
+  createActionCopy (action: SoftwareUpdateAction): void {
+    this.actionCopy = SoftwareUpdateAction.createFromObject(action)
+  }
+
+  @Watch('value', { immediate: true, deep: true })
+  // @ts-ignore
+  onValueChanged (val: SoftwareUpdateAction) {
+    this.createActionCopy(val)
+  }
+}
+</script>
diff --git a/modelUtils/Compareables.ts b/modelUtils/Compareables.ts
index bbd815b9a..2d260efd6 100644
--- a/modelUtils/Compareables.ts
+++ b/modelUtils/Compareables.ts
@@ -9,7 +9,7 @@ export interface IDateCompareable {
 }
 
 export function isDateCompareable (i: any): i is IDateCompareable {
-  if (i && typeof i === 'object' && Object.prototype.hasOwnProperty.call(i, 'date') && DateTime.isDateTime(i.date)) {
+  if (i && typeof i === 'object' && 'date' in i && DateTime.isDateTime(i.date)) {
     return true
   }
   return false
diff --git a/models/ActionCommonDetails.ts b/models/ActionCommonDetails.ts
new file mode 100644
index 000000000..b3949f6df
--- /dev/null
+++ b/models/ActionCommonDetails.ts
@@ -0,0 +1,95 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { Attachment } from '@/models/Attachment'
+import { Contact } from '@/models/Contact'
+
+export interface IActionCommonDetails {
+  id: string | null
+  description: string
+  contact: Contact | null
+  attachments: Attachment[]
+}
+
+/**
+ * a very unspecific Action class
+ *
+ * this class in mainly used for inheritance in derived classes
+ *
+ * @implements IActionCommonDetails
+ */
+export class ActionCommonDetails implements IActionCommonDetails {
+  private _id: string | null = null
+  private _description: string = ''
+  private _contact: Contact | null = null
+  private _attachments: Attachment[] = []
+
+  get id (): string | null {
+    return this._id
+  }
+
+  set id (id: string | null) {
+    this._id = id
+  }
+
+  get description (): string {
+    return this._description
+  }
+
+  set description (description: string) {
+    this._description = description
+  }
+
+  get contact (): Contact | null {
+    return this._contact
+  }
+
+  set contact (contact: Contact | null) {
+    this._contact = contact
+  }
+
+  get attachments (): Attachment[] {
+    return this._attachments
+  }
+
+  set attachments (attachments: Attachment[]) {
+    this._attachments = attachments
+  }
+
+  static createFromObject (value: IActionCommonDetails): ActionCommonDetails {
+    const action = new ActionCommonDetails()
+    action.id = value.id
+    action.description = value.description
+    action.contact = value.contact ? Contact.createFromObject(value.contact) : null
+    action.attachments = value.attachments.map(a => Attachment.createFromObject(a))
+    return action
+  }
+}
diff --git a/models/GenericAction.ts b/models/GenericAction.ts
index 4d144df1f..a7892d887 100644
--- a/models/GenericAction.ts
+++ b/models/GenericAction.ts
@@ -32,27 +32,21 @@
 import { DateTime } from 'luxon'
 import { Attachment } from '@/models/Attachment'
 import { Contact } from '@/models/Contact'
-import { IAction } from '@/models/Action'
+import { IActionCommonDetails, ActionCommonDetails } from '@/models/ActionCommonDetails'
 import { IDateCompareable } from '@/modelUtils/Compareables'
 
-export interface IGenericAction extends IAction {
+export interface IGenericAction extends IActionCommonDetails {
   actionTypeName: string
   actionTypeUrl: string
   beginDate: DateTime | null
   endDate: DateTime | null
-  contact: Contact | null
-  attachments: Attachment[]
 }
 
-export class GenericAction implements IGenericAction, IDateCompareable {
-  private _id: string | null = null
-  private _description: string = ''
+export class GenericAction extends ActionCommonDetails implements IGenericAction, IDateCompareable {
   private _actionTypeName: string = ''
   private _actionTypeUrl: string = ''
   private _beginDate: DateTime | null = null
   private _endDate: DateTime | null = null
-  private _contact: Contact | null = null
-  private _attachments: Attachment[] = []
 
   /**
    * returns an empty instance
@@ -77,29 +71,13 @@ export class GenericAction implements IGenericAction, IDateCompareable {
     action.description = someObject.description
     action.actionTypeName = someObject.actionTypeName
     action.actionTypeUrl = someObject.actionTypeUrl
-    action.beginDate = someObject.beginDate ? someObject.beginDate : null
-    action.endDate = someObject.endDate ? someObject.endDate : null
+    action.beginDate = someObject.beginDate
+    action.endDate = someObject.endDate
     action.contact = someObject.contact ? Contact.createFromObject(someObject.contact) : null
     action.attachments = someObject.attachments.map(i => Attachment.createFromObject(i))
     return action
   }
 
-  get id (): string | null {
-    return this._id
-  }
-
-  set id (id: string | null) {
-    this._id = id
-  }
-
-  get description (): string {
-    return this._description
-  }
-
-  set description (description: string) {
-    this._description = description
-  }
-
   get actionTypeUrl (): string {
     return this._actionTypeUrl
   }
@@ -132,22 +110,6 @@ export class GenericAction implements IGenericAction, IDateCompareable {
     this._endDate = date
   }
 
-  get contact (): Contact | null {
-    return this._contact
-  }
-
-  set contact (contact: Contact | null) {
-    this._contact = contact
-  }
-
-  get attachments (): Attachment[] {
-    return this._attachments
-  }
-
-  set attachments (attachments: Attachment[]) {
-    this._attachments = attachments
-  }
-
   get isGenericAction (): boolean {
     return true
   }
diff --git a/models/SoftwareType.ts b/models/SoftwareType.ts
new file mode 100644
index 000000000..1f23ccb68
--- /dev/null
+++ b/models/SoftwareType.ts
@@ -0,0 +1,100 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+export interface ISoftwareType {
+  id: string
+  name: string
+  uri: string
+  definition: string
+}
+
+export class SoftwareType implements ISoftwareType {
+  private _id: string = ''
+  private _name: string = ''
+  private _uri: string = ''
+  private _definition: string = ''
+
+  get id (): string {
+    return this._id
+  }
+
+  set id (newId: string) {
+    this._id = newId
+  }
+
+  get name (): string {
+    return this._name
+  }
+
+  set name (newName: string) {
+    this._name = newName
+  }
+
+  get uri (): string {
+    return this._uri
+  }
+
+  set uri (newUri: string) {
+    this._uri = newUri
+  }
+
+  get definition (): string {
+    return this._definition
+  }
+
+  set definition (newDefinition: string) {
+    this._definition = newDefinition
+  }
+
+  toString (): string {
+    return this._name
+  }
+
+  static createWithData (id: string, name: string, uri: string, definition: string): SoftwareType {
+    const result = new SoftwareType()
+    result.id = id
+    result.name = name
+    result.uri = uri
+    result.definition = definition
+    return result
+  }
+
+  static createFromObject (someObject: ISoftwareType): SoftwareType {
+    const newObject = new SoftwareType()
+
+    newObject.id = someObject.id
+    newObject.name = someObject.name
+    newObject.uri = someObject.uri
+    newObject.definition = someObject.definition
+
+    return newObject
+  }
+}
diff --git a/models/SoftwareUpdateAction.ts b/models/SoftwareUpdateAction.ts
new file mode 100644
index 000000000..1948a44ef
--- /dev/null
+++ b/models/SoftwareUpdateAction.ts
@@ -0,0 +1,131 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+import { Attachment } from '@/models/Attachment'
+import { Contact } from '@/models/Contact'
+import { IActionCommonDetails, ActionCommonDetails } from '@/models/ActionCommonDetails'
+import { IDateCompareable } from '@/modelUtils/Compareables'
+
+export interface ISoftwareUpdateAction extends IActionCommonDetails {
+  softwareTypeName: string
+  softwareTypeUrl: string
+  updateDate: DateTime | null
+  version: string
+  repositoryUrl: string
+}
+
+export class SoftwareUpdateAction extends ActionCommonDetails implements ISoftwareUpdateAction, IDateCompareable {
+  private _softwareTypeName: string = ''
+  private _softwareTypeUrl: string = ''
+  private _updateDate: DateTime | null = null
+  private _version: string = ''
+  private _repositoryUrl: string = ''
+
+  /**
+   * returns an empty instance
+   *
+   * @static
+   * @return {SoftwareUpdateAction} an empty instance
+   */
+  static createEmpty (): SoftwareUpdateAction {
+    return new SoftwareUpdateAction()
+  }
+
+  /**
+   * creates an instance from an existing ISoftwareUpdateAction-like object
+   *
+   * @static
+   * @param {ISoftwareUpdateAction} someObject - an ISoftwareUpdateAction like object
+   * @return {SoftwareUpdateAction} a cloned instance of the original object
+   */
+  static createFromObject (someObject: ISoftwareUpdateAction) : SoftwareUpdateAction {
+    const action = new SoftwareUpdateAction()
+    action.id = someObject.id
+    action.softwareTypeName = someObject.softwareTypeName
+    action.softwareTypeUrl = someObject.softwareTypeUrl
+    action.updateDate = someObject.updateDate
+    action.version = someObject.version
+    action.repositoryUrl = someObject.repositoryUrl
+    action.description = someObject.description
+    action.contact = someObject.contact ? Contact.createFromObject(someObject.contact) : null
+    action.attachments = someObject.attachments.map(i => Attachment.createFromObject(i))
+    return action
+  }
+
+  get softwareTypeUrl (): string {
+    return this._softwareTypeUrl
+  }
+
+  set softwareTypeUrl (softwareTypeUrl: string) {
+    this._softwareTypeUrl = softwareTypeUrl
+  }
+
+  get softwareTypeName (): string {
+    return this._softwareTypeName
+  }
+
+  set softwareTypeName (softwareTypeName: string) {
+    this._softwareTypeName = softwareTypeName
+  }
+
+  get updateDate (): DateTime | null {
+    return this._updateDate
+  }
+
+  set updateDate (date: DateTime | null) {
+    this._updateDate = date
+  }
+
+  get version (): string {
+    return this._version
+  }
+
+  set version (version: string) {
+    this._version = version
+  }
+
+  get repositoryUrl (): string {
+    return this._repositoryUrl
+  }
+
+  set repositoryUrl (repositoryUrl: string) {
+    this._repositoryUrl = repositoryUrl
+  }
+
+  get isSoftwareUpdateAction (): boolean {
+    return true
+  }
+
+  get date (): DateTime | null {
+    return this.updateDate
+  }
+}
diff --git a/pages/configurations/_id.vue b/pages/configurations/_id.vue
index 6fba723e1..fe6e8e18e 100644
--- a/pages/configurations/_id.vue
+++ b/pages/configurations/_id.vue
@@ -353,7 +353,7 @@ export default class ConfigurationsIdPage extends Vue {
   }
 
   validateInputForStartDate (v: string): boolean | string {
-    if (v === null || v === '') {
+    if (v === null || v === '' || this.configuration.startDate === null) {
       return true
     }
     if (!this.configuration.endDate) {
@@ -366,7 +366,7 @@ export default class ConfigurationsIdPage extends Vue {
   }
 
   validateInputForEndDate (v: string): boolean | string {
-    if (v === null || v === '') {
+    if (v === null || v === '' || this.configuration.endDate === null) {
       return true
     }
     if (!this.configuration.startDate) {
diff --git a/pages/devices/_deviceId/actions.vue b/pages/devices/_deviceId/actions.vue
index da0005cea..07d1c7c3d 100644
--- a/pages/devices/_deviceId/actions.vue
+++ b/pages/devices/_deviceId/actions.vue
@@ -56,11 +56,9 @@ permissions and limitations under the Licence.
     <template
       v-else-if="isEditActionPage"
     >
-      <!-- the currently edited action is passed as a property to the
-           `edit.vue` page to avoid reloading of the action -->
       <NuxtChild
-        :value="editedAction"
         @input="$fetch"
+        @showload="showload"
         @showsave="showsave"
       />
     </template>
@@ -77,7 +75,10 @@ permissions and limitations under the Licence.
             v-if="action.isGenericAction"
             v-model="actions[index]"
           >
-            <template #menu>
+            <template
+              v-if="isLoggedIn"
+              #menu
+            >
               <v-menu
                 close-on-click
                 close-on-content-click
@@ -105,7 +106,7 @@ permissions and limitations under the Licence.
                   <v-list-item
                     :disabled="!isLoggedIn"
                     dense
-                    @click="showDeleteDialog(action.id)"
+                    @click="showDeleteDialog(action)"
                   >
                     <v-list-item-content>
                       <v-list-item-title
@@ -127,7 +128,8 @@ permissions and limitations under the Licence.
             </template>
             <template #actions>
               <v-btn
-                :to="'/devices/' + deviceId + '/actions/' + action.id + '/edit'"
+                v-if="isLoggedIn"
+                :to="'/devices/' + deviceId + '/actions/generic-device-actions/' + action.id + '/edit'"
                 color="primary"
                 text
                 @click.stop.prevent
@@ -137,65 +139,74 @@ permissions and limitations under the Licence.
             </template>
           </GenericActionCard>
 
-          <template v-if="action.isDeviceSoftwareUpdateAction">
-            <v-card>
-              <v-card-subtitle class="pb-0">
-                {{ action.updateDate | toUtcDate }}
-              </v-card-subtitle>
-              <v-card-title class="pt-0">
-                {{ action.softwareTypeName }} update
-              </v-card-title>
-              <v-card-subtitle>
-                <v-row
-                  no-gutters
-                >
-                  <v-col
-                    cols="11"
-                  >
-                    {{ action.contact.toString() }}
-                  </v-col>
-                  <v-col
-                    align-self="end"
-                    class="text-right"
+          <SoftwareUpdateActionCard
+            v-if="action.isSoftwareUpdateAction"
+            v-model="actions[index]"
+          >
+            <template
+              v-if="isLoggedIn"
+              #menu
+            >
+              <v-menu
+                close-on-click
+                close-on-content-click
+                offset-x
+                left
+                z-index="999"
+              >
+                <template #activator="{ on }">
+                  <v-btn
+                    data-role="property-menu"
+                    icon
+                    small
+                    v-on="on"
                   >
-                    <v-btn
-                      icon
-                      @click.stop.prevent="showActionItem(action.id)"
+                    <v-icon
+                      dense
+                      small
                     >
-                      <v-icon>{{ isActionItemShown(action.id) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
-                    </v-btn>
-                  </v-col>
-                </v-row>
-              </v-card-subtitle>
-              <v-expand-transition>
-                <v-card-text
-                  v-show="isActionItemShown(action.id)"
-                  class="text--primary"
-                >
-                  <v-row dense>
-                    <v-col cols="12" md="4">
-                      <label>
-                        Version
-                      </label>
-                      {{ action.version }}
-                    </v-col>
-                    <v-col cols="12" md="4">
-                      <label>
-                        Repository
-                      </label>
-                      {{ action.repositoryUrl }}
-                    </v-col>
-                  </v-row>
-                  <v-row dense>
-                    <v-col>
-                      <label>Description</label>
-                      {{ action.description }}
-                    </v-col>
-                  </v-row>
-                </v-card-text>
-              </v-expand-transition>
-            </v-card>
-          </template>
+                      mdi-dots-vertical
+                    </v-icon>
+                  </v-btn>
+                </template>
+
+                <v-list>
+                  <v-list-item
+                    :disabled="!isLoggedIn"
+                    dense
+                    @click="showDeleteDialog(action)"
+                  >
+                    <v-list-item-content>
+                      <v-list-item-title
+                        :class="isLoggedIn ? 'red--text' : 'grey--text'"
+                      >
+                        <v-icon
+                          left
+                          small
+                          :color="isLoggedIn ? 'red' : 'grey'"
+                        >
+                          mdi-delete
+                        </v-icon>
+                        Delete
+                      </v-list-item-title>
+                    </v-list-item-content>
+                  </v-list-item>
+                </v-list>
+              </v-menu>
+            </template>
+            <template #actions>
+              <v-btn
+                v-if="isLoggedIn"
+                :to="'/devices/' + deviceId + '/actions/software-update-actions/' + action.id + '/edit'"
+                color="primary"
+                text
+                @click.stop.prevent
+              >
+                Edit
+              </v-btn>
+            </template>
+          </SoftwareUpdateActionCard>
+
           <template v-if="action.isDeviceCalibrationAction">
             <v-card>
               <v-card-subtitle class="pb-0">
@@ -348,7 +359,11 @@ permissions and limitations under the Licence.
           </template>
         </v-timeline-item>
       </v-timeline>
-      <v-dialog v-model="hasActionIdToDelete" max-width="290">
+      <v-dialog
+        v-model="hasActionToDelete"
+        max-width="290"
+        @click:outside="hideDeleteDialog"
+      >
         <v-card>
           <v-card-title class="headline">
             Delete action
@@ -367,7 +382,7 @@ permissions and limitations under the Licence.
             <v-btn
               color="error"
               text
-              @click="deleteActionAndCloseDialog(actionIdToDelete)"
+              @click="deleteAction()"
             >
               <v-icon left>
                 mdi-delete
@@ -384,13 +399,16 @@ permissions and limitations under the Licence.
 import { Component, Vue } from 'nuxt-property-decorator'
 import ProgressIndicator from '@/components/ProgressIndicator.vue'
 import GenericActionCard from '@/components/GenericActionCard.vue'
+import SoftwareUpdateActionCard from '@/components/SoftwareUpdateActionCard.vue'
 
 import { DateTime } from 'luxon'
 
+import { Attachment } from '@/models/Attachment'
 import { Contact } from '@/models/Contact'
 import { DeviceProperty } from '@/models/DeviceProperty'
-import { IAction } from '@/models/Action'
+import { IActionCommonDetails } from '@/models/ActionCommonDetails'
 import { GenericAction } from '@/models/GenericAction'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
 import { DateComparator, isDateCompareable } from '@/modelUtils/Compareables'
 
 const toUtcDate = (dt: DateTime) => {
@@ -401,49 +419,7 @@ interface IColoredAction {
   getColor (): string
 }
 
-class DeviceSoftwareUpdateAction implements IAction, IColoredAction {
-  public id: string
-  public softwareTypeName: string
-  public softwareTypeUri: string
-  public updateDate: DateTime
-  public version: string
-  public repositoryUrl: string
-  public description: string
-  public contact: Contact
-  constructor (
-    id: string,
-    softwareTypeName: string,
-    softwareTypeUri: string,
-    updateDate: DateTime,
-    version: string,
-    repositoryUrl: string,
-    description: string,
-    contact: Contact
-  ) {
-    this.id = id
-    this.softwareTypeName = softwareTypeName
-    this.softwareTypeUri = softwareTypeUri
-    this.updateDate = updateDate
-    this.version = version
-    this.repositoryUrl = repositoryUrl
-    this.description = description
-    this.contact = contact
-  }
-
-  getId (): string {
-    return 'software-' + this.id
-  }
-
-  get isDeviceSoftwareUpdateAction (): boolean {
-    return true
-  }
-
-  getColor (): string {
-    return 'yellow'
-  }
-}
-
-class DeviceCalibrationAction implements IAction, IColoredAction {
+class DeviceCalibrationAction implements IActionCommonDetails, IColoredAction {
   public id: string
   public description: string
   public currentCalibrationDate: DateTime
@@ -452,6 +428,7 @@ class DeviceCalibrationAction implements IAction, IColoredAction {
   public value: string
   public deviceProperties: DeviceProperty[]
   public contact: Contact
+  public attachments: Attachment[]
   constructor (
     id: string,
     description: string,
@@ -460,7 +437,8 @@ class DeviceCalibrationAction implements IAction, IColoredAction {
     formula: string,
     value: string,
     deviceProperties: DeviceProperty[],
-    contact: Contact
+    contact: Contact,
+    attachments: Attachment[]
   ) {
     this.id = id
     this.description = description
@@ -470,6 +448,7 @@ class DeviceCalibrationAction implements IAction, IColoredAction {
     this.value = value
     this.deviceProperties = deviceProperties
     this.contact = contact
+    this.attachments = attachments
   }
 
   getId (): string {
@@ -495,6 +474,7 @@ class DeviceMountAction {
   public beginDate: DateTime
   public description: string
   public contact: Contact
+  public attachments: Attachment[]
   constructor (
     id: string,
     configurationName: string,
@@ -504,7 +484,8 @@ class DeviceMountAction {
     offsetZ: number,
     beginDate: DateTime,
     description: string,
-    contact: Contact
+    contact: Contact,
+    attachments: Attachment[]
   ) {
     this.id = id
     this.configurationName = configurationName
@@ -515,6 +496,7 @@ class DeviceMountAction {
     this.beginDate = beginDate
     this.description = description
     this.contact = contact
+    this.attachments = attachments
   }
 
   getId (): string {
@@ -530,24 +512,27 @@ class DeviceMountAction {
   }
 }
 
-class DeviceUnmountAction implements IAction, IColoredAction {
+class DeviceUnmountAction implements IActionCommonDetails, IColoredAction {
   public id: string
   public configurationName: string
   public endDate: DateTime
   public description: string
   public contact: Contact
+  public attachments: Attachment[]
   constructor (
     id: string,
     configurationName: string,
     endDate: DateTime,
     description: string,
-    contact: Contact
+    contact: Contact,
+    attachments: Attachment[]
   ) {
     this.id = id
     this.configurationName = configurationName
     this.endDate = endDate
     this.description = description
     this.contact = contact
+    this.attachments = attachments
   }
 
   getId (): string {
@@ -564,18 +549,27 @@ class DeviceUnmountAction implements IAction, IColoredAction {
 }
 
 /**
- * extend the original interface by adding the getColor() method
+ * extend the original interfaces by adding the getColor() method
  */
 declare module '@/models/GenericAction' {
-  export interface GenericAction extends IColoredAction {
+  interface GenericAction extends IColoredAction {
   }
 }
 GenericAction.prototype.getColor = (): string => 'blue'
 
+declare module '@/models/SoftwareUpdateAction' {
+  interface SoftwareUpdateAction extends IColoredAction {
+  }
+}
+SoftwareUpdateAction.prototype.getColor = (): string => 'yellow'
+
+type ActionDeleteMethod = (id: string) => Promise<void>
+
 @Component({
   components: {
     ProgressIndicator,
-    GenericActionCard
+    GenericActionCard,
+    SoftwareUpdateActionCard
   },
   filters: {
     toUtcDate
@@ -585,10 +579,10 @@ export default class DeviceActionsPage extends Vue {
   private isLoading: boolean = false
   private isSaving: boolean = false
 
-  private actions: IAction[] = []
+  private actions: IActionCommonDetails[] = []
   private searchResultItemsShown: { [id: string]: boolean } = {}
 
-  private actionIdToDelete: string = ''
+  private actionToDelete: IActionCommonDetails | null = null
 
   async fetch () {
     const contact1 = Contact.createFromObject({
@@ -605,16 +599,6 @@ export default class DeviceActionsPage extends Vue {
       email: 'cam.paign@gfz-potsdam.de',
       website: ''
     })
-    const deviceSoftwareUpdateAction = new DeviceSoftwareUpdateAction(
-      'X2',
-      'Firmware',
-      'softwaretypes/firmware',
-      DateTime.fromISO('2021-03-30T08:10:00Z'),
-      '1.0.34',
-      'git.gfz-potsdam.de/sensor-management-system/firmware',
-      'The 1.0.34 firmware version for the device',
-      contact1
-    )
     const devProp1 = new DeviceProperty()
     devProp1.label = 'Wind direction'
     const devProp2 = new DeviceProperty()
@@ -627,7 +611,8 @@ export default class DeviceActionsPage extends Vue {
       'f(x) = x',
       '100',
       [devProp1, devProp2],
-      contact2
+      contact2,
+      []
     )
     const deviceMountAction1 = new DeviceMountAction(
       'X4',
@@ -638,35 +623,39 @@ export default class DeviceActionsPage extends Vue {
       2,
       DateTime.fromISO('2021-03-30T12:00:00Z'),
       'Mounted Measurement ABC',
-      contact1
+      contact1,
+      []
     )
     const deviceUnmountAction1 = new DeviceUnmountAction(
       'X5',
       'Measurement ABC',
       DateTime.fromISO('2022-03-30T12:00:00Z'),
       'Unmounted Measurement ABC',
-      contact1
+      contact1,
+      []
     )
     this.actions = [
-      deviceSoftwareUpdateAction,
       deviceCalibrationAction1,
       deviceMountAction1,
       deviceUnmountAction1
     ]
-    await Promise.all([this.fetchGenericActions()])
+    await Promise.all([
+      this.fetchGenericActions(),
+      this.fetchSoftwareUpdateActions()
+    ])
 
     // sort the actions
     const comparator = new DateComparator()
-    this.actions.sort((i: IAction, j: IAction): number => {
+    this.actions.sort((i: IActionCommonDetails, j: IActionCommonDetails): number => {
       if (isDateCompareable(i) && isDateCompareable(j)) {
         // multiply result with -1 to get descending order
         return comparator.compare(i, j) * -1
       }
       if (isDateCompareable(i)) {
-        return 1
+        return -1
       }
       if (isDateCompareable(j)) {
-        return -1
+        return 1
       }
       return 0
     })
@@ -677,6 +666,11 @@ export default class DeviceActionsPage extends Vue {
     actions.forEach((action: GenericAction) => this.actions.push(action))
   }
 
+  async fetchSoftwareUpdateActions (): Promise<void> {
+    const actions: SoftwareUpdateAction[] = await this.$api.devices.findRelatedSoftwareUpdateActions(this.deviceId)
+    actions.forEach((action: SoftwareUpdateAction) => this.actions.push(action))
+  }
+
   get isInProgress (): boolean {
     return this.isLoading || this.isSaving
   }
@@ -700,7 +694,7 @@ export default class DeviceActionsPage extends Vue {
 
   get isEditActionPage (): boolean {
     // eslint-disable-next-line no-useless-escape
-    const editUrl = '^\/devices\/' + this.deviceId + '\/actions\/([0-9]+)\/edit$'
+    const editUrl = '^\/devices\/' + this.deviceId + '\/actions\/[a-zA-Z-]+\/[0-9]+\/edit$'
     return !!this.$route.path.match(editUrl)
   }
 
@@ -718,45 +712,61 @@ export default class DeviceActionsPage extends Vue {
     return this.$route.params.actionId
   }
 
-  /**
-   * Returns the action object from the list of actions that is currently edited
-   *
-   * When the `/edit` route for an action is called, the `edit.vue` page is
-   * included via a `NuxtChild` component. To avoid of loading the action again
-   * in this page, we return it from this method to pass it as a property to
-   * the `NuxtChild` component
-   *
-   * Calls {@link DeviceActionsPage.actionId} to get the currently edited
-   * action from the route.
-   *
-   * @return {IAction | undefined} the found action, otherwise undefined
-   */
-  get editedAction (): IAction | undefined {
-    if (!this.actionId) {
-      return
-    }
-    return this.actions.find(action => action.id === this.actionId)
-  }
-
-  get hasActionIdToDelete (): boolean {
-    return !!this.actionIdToDelete
+  get hasActionToDelete (): boolean {
+    return this.actionToDelete !== null
   }
 
   showsave (isSaving: boolean) {
     this.isSaving = isSaving
   }
 
-  showDeleteDialog (id: string) {
-    this.actionIdToDelete = id
+  showload (isLoading: boolean) {
+    this.isLoading = isLoading
+  }
+
+  showDeleteDialog (action: IActionCommonDetails) {
+    this.actionToDelete = action
   }
 
   hideDeleteDialog (): void {
-    this.actionIdToDelete = ''
+    this.actionToDelete = null
+  }
+
+  /**
+   * deletes the action and closes the delete dialog
+   *
+   * calls {@link DeviceActionsPage#deleteActionAndCloseDialog} with the appropriate API method
+   *
+   * @throws {TypeError} - throws an Error if the type of action is not supported
+   */
+  deleteAction (): void {
+    if (!this.actionToDelete) {
+      return
+    }
+    if (!this.actionToDelete.id) {
+      return
+    }
+    switch (true) {
+      case 'isGenericAction' in this.actionToDelete:
+        this.deleteActionAndCloseDialog(this.actionToDelete.id, this.$api.genericDeviceActions.deleteById.bind(this.$api.genericDeviceActions))
+        break
+      case 'isSoftwareUpdateAction' in this.actionToDelete:
+        this.deleteActionAndCloseDialog(this.actionToDelete.id, this.$api.deviceSoftwareUpdateActions.deleteById.bind(this.$api.deviceSoftwareUpdateActions))
+        break
+      default:
+        throw new TypeError('deleting the action type is not supported.')
+    }
   }
 
-  deleteActionAndCloseDialog (id: string) {
+  /**
+   * deletes the action and closes the delete dialog
+   *
+   * @param {string} id - the id of the action to delete
+   * @param {ActionDeleteMethod} actionDeleteMethod - an API method to delete an action
+   */
+  deleteActionAndCloseDialog (id: string, actionDeleteMethod: ActionDeleteMethod): void {
     this.isSaving = true
-    this.$api.genericDeviceActions.deleteById(id).then(() => {
+    actionDeleteMethod(id).then(() => {
       this.$fetch()
       this.$store.commit('snackbar/setSuccess', 'Action deleted')
     }).catch((_error) => {
@@ -767,11 +777,11 @@ export default class DeviceActionsPage extends Vue {
     })
   }
 
-  getActionType (action: IAction): string {
+  getActionType (action: IActionCommonDetails): string {
     switch (true) {
       case 'isGenericAction' in action:
         return 'generic-action'
-      case 'isDeviceSoftwareUpdateAction' in action:
+      case 'isSoftwareUpdateAction' in action:
         return 'software-update-action'
       case 'isDeviceCalibrationAction' in action:
         return 'device-calibration-action'
@@ -780,7 +790,7 @@ export default class DeviceActionsPage extends Vue {
     }
   }
 
-  getActionTypeIterationKey (action: IAction): string {
+  getActionTypeIterationKey (action: IActionCommonDetails): string {
     return this.getActionType(action) + '-' + action.id
   }
 }
diff --git a/pages/devices/_deviceId/actions/_actionId/edit.vue b/pages/devices/_deviceId/actions/generic-device-actions/_actionId/edit.vue
similarity index 77%
rename from pages/devices/_deviceId/actions/_actionId/edit.vue
rename to pages/devices/_deviceId/actions/generic-device-actions/_actionId/edit.vue
index 52eda9234..8d0e1e89d 100644
--- a/pages/devices/_deviceId/actions/_actionId/edit.vue
+++ b/pages/devices/_deviceId/actions/generic-device-actions/_actionId/edit.vue
@@ -51,19 +51,21 @@ permissions and limitations under the Licence.
         apply
       </v-btn>
     </v-card-actions>
+
     <!-- just to be consistent with the new mask, we show the selected action type as an disabled v-select here -->
     <v-select
-      :value="valueCopy.actionTypeName"
-      :items="[valueCopy.actionTypeName]"
+      :value="action.actionTypeName"
+      :items="[action.actionTypeName]"
       :item-text="(x) => x"
       disabled
       label="Action Type"
     />
     <GenericActionForm
       ref="genericDeviceActionForm"
-      v-model="valueCopy"
+      v-model="action"
       :attachments="attachments"
     />
+
     <v-card-actions>
       <v-spacer />
       <v-btn
@@ -89,7 +91,7 @@ permissions and limitations under the Licence.
 </template>
 
 <script lang="ts">
-import { Component, Vue, Prop, Watch } from 'nuxt-property-decorator'
+import { Component, Vue } from 'nuxt-property-decorator'
 
 import { Attachment } from '@/models/Attachment'
 import { GenericAction } from '@/models/GenericAction'
@@ -99,27 +101,33 @@ import GenericActionForm from '@/components/GenericActionForm.vue'
 @Component({
   components: {
     GenericActionForm
-  }
+  },
+  scrollToTop: true
 })
-export default class DeviceActionEditPage extends Vue {
-  private valueCopy: GenericAction = new GenericAction()
+export default class GenericDeviceActionEditPage extends Vue {
+  private action: GenericAction = new GenericAction()
   private attachments: Attachment[] = []
+  private _isLoading: boolean = false
   private _isSaving: boolean = false
 
-  @Prop({
-    default: () => new GenericAction(),
-    required: true,
-    type: Object
-  })
-  readonly value!: GenericAction
+  async fetch (): Promise<any> {
+    this.isLoading = true
+    await Promise.all([
+      this.fetchAttachments(),
+      this.fetchAction()
+    ])
+    this.isLoading = false
+  }
 
-  created () {
-    if (this.value) {
-      this.valueCopy = GenericAction.createFromObject(this.value)
+  async fetchAction (): Promise<any> {
+    try {
+      this.action = await this.$api.genericDeviceActions.findById(this.actionId)
+    } catch (_) {
+      this.$store.commit('snackbar/setError', 'Failed to fetch action')
     }
   }
 
-  async fetch (): Promise<any> {
+  async fetchAttachments (): Promise<any> {
     try {
       this.attachments = await this.$api.devices.findRelatedDeviceAttachments(this.deviceId)
     } catch (_) {
@@ -131,10 +139,23 @@ export default class DeviceActionEditPage extends Vue {
     return this.$route.params.deviceId
   }
 
+  get actionId (): string {
+    return this.$route.params.actionId
+  }
+
   get isLoggedIn (): boolean {
     return this.$store.getters['oidc/isAuthenticated']
   }
 
+  get isLoading (): boolean {
+    return this.$data._isLoading
+  }
+
+  set isLoading (value: boolean) {
+    this.$data._isLoading = value
+    this.$emit('showload', value)
+  }
+
   get isSaving (): boolean {
     return this.$data._isSaving
   }
@@ -150,7 +171,7 @@ export default class DeviceActionEditPage extends Vue {
       return
     }
     this.isSaving = true
-    this.$api.genericDeviceActions.update(this.deviceId, this.valueCopy).then((action: GenericAction) => {
+    this.$api.genericDeviceActions.update(this.deviceId, this.action).then((action: GenericAction) => {
       this.$router.push('/devices/' + this.deviceId + '/actions', () => this.$emit('input', action))
     }).catch(() => {
       this.$store.commit('snackbar/setError', 'Failed to save the action')
@@ -158,11 +179,5 @@ export default class DeviceActionEditPage extends Vue {
       this.isSaving = false
     })
   }
-
-  @Watch('value', { immediate: true, deep: true })
-  // @ts-ignore
-  onValueChanged (val: GenericAction) {
-    this.valueCopy = GenericAction.createFromObject(val)
-  }
 }
 </script>
diff --git a/pages/devices/_deviceId/actions/new.vue b/pages/devices/_deviceId/actions/new.vue
index dc3320194..e192833f3 100644
--- a/pages/devices/_deviceId/actions/new.vue
+++ b/pages/devices/_deviceId/actions/new.vue
@@ -58,7 +58,7 @@ permissions and limitations under the Licence.
           v-else-if="softwareUpdateChosen"
           color="green"
           small
-          @click="addDeviceSoftwareUpdateAction"
+          @click="addSoftwareUpdateAction"
         >
           Add
         </v-btn>
@@ -132,46 +132,19 @@ permissions and limitations under the Licence.
           </v-row>
         </v-form>
       </v-card-text>
+
       <!-- softwareUpdate -->
       <v-card-text
         v-if="softwareUpdateChosen"
       >
-        <v-form
-          ref="datesForm"
-          v-model="datesAreValid"
-          @submit.prevent
-        >
-          <v-row>
-            <v-col cols="12" md="6">
-              <DatePicker
-                :value="startDate"
-                label="Date"
-                :rules="[rules.startDate, rules.updateDateNotNull]"
-                @input="setStartDateAndValidate"
-              />
-            </v-col>
-          </v-row>
-        </v-form>
-        <v-form
-          ref="softwareTypeForm"
-          v-model="softwareTypeIsValid"
-          @submit.prevent
-        >
-          <v-row>
-            <v-col cols="12" md="6">
-              <v-select :items="softwareTypes" clearable :item-text="(x) => x.name" label="Software type" :rules="[rules.softwareTypeNotNull]" />
-            </v-col>
-          </v-row>
-        </v-form>
-        <v-row>
-          <v-col cols="12" md="3">
-            <v-text-field label="Version" placeholder="1.2.3" />
-          </v-col>
-          <v-col cols="12" md="9">
-            <v-text-field label="Repository URL" placeholder="https://github.com/" />
-          </v-col>
-        </v-row>
+        <SoftwareUpdateActionForm
+          ref="softwareUpdateActionForm"
+          v-model="softwareUpdateAction"
+          :attachments="attachments"
+        />
       </v-card-text>
+
+      <!-- genericAction -->
       <v-card-text
         v-if="genericActionChosen"
       >
@@ -181,9 +154,13 @@ permissions and limitations under the Licence.
           :attachments="attachments"
         />
       </v-card-text>
-      <!-- action type independent -->
+
+      <!-- Action type independent
+           TODO: can be removed once all Action classes are implemented and
+           derive from ActionCommonDetails. Then the CommonActionForm component can
+           be used for all Action types. -->
       <v-card-text
-        v-if="chosenKindOfAction && !genericActionChosen"
+        v-if="chosenKindOfAction && !genericActionChosen && !softwareUpdateChosen"
       >
         <v-row>
           <v-col cols="12" md="12">
@@ -255,7 +232,7 @@ permissions and limitations under the Licence.
           v-else-if="softwareUpdateChosen"
           color="green"
           small
-          @click="addDeviceSoftwareUpdateAction"
+          @click="addSoftwareUpdateAction"
         >
           Add
         </v-btn>
@@ -281,6 +258,7 @@ import { Contact } from '@/models/Contact'
 import { Attachment } from '@/models/Attachment'
 import { DeviceProperty } from '@/models/DeviceProperty'
 import { GenericAction } from '@/models/GenericAction'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
 import { IActionType, ActionType } from '@/models/ActionType'
 
 import { ACTION_TYPE_API_FILTER_DEVICE } from '@/services/cv/ActionTypeApi'
@@ -288,6 +266,7 @@ import { ACTION_TYPE_API_FILTER_DEVICE } from '@/services/cv/ActionTypeApi'
 import { dateToString, stringToDate } from '@/utils/dateHelper'
 
 import GenericActionForm from '@/components/GenericActionForm.vue'
+import SoftwareUpdateActionForm from '@/components/SoftwareUpdateActionForm.vue'
 import DatePicker from '@/components/DatePicker.vue'
 
 const KIND_OF_ACTION_TYPE_DEVICE_CALIBRATION = 'device_calibration'
@@ -303,6 +282,7 @@ type IOptionsForActionType = Pick<IActionType, 'id' | 'name' | 'uri'> & {
 @Component({
   components: {
     GenericActionForm,
+    SoftwareUpdateActionForm,
     DatePicker
   }
 })
@@ -359,6 +339,7 @@ export default class ActionAddPage extends Vue {
   private endDate: DateTime | null = null
 
   private genericDeviceAction: GenericAction = new GenericAction()
+  private softwareUpdateAction: SoftwareUpdateAction = new SoftwareUpdateAction()
 
   private _isSaving: boolean = false
 
@@ -417,6 +398,9 @@ export default class ActionAddPage extends Vue {
         this.genericDeviceAction.actionTypeName = newValue?.name || ''
         this.genericDeviceAction.actionTypeUrl = newValue?.uri || ''
       }
+      if (this.softwareUpdateChosen) {
+        this.softwareUpdateAction = new SoftwareUpdateAction()
+      }
     }
   }
 
@@ -543,17 +527,26 @@ export default class ActionAddPage extends Vue {
     this.$store.commit('snackbar/setError', 'Not implemented yet')
   }
 
-  addDeviceSoftwareUpdateAction () {
-    if (!(this.$refs.datesForm as Vue & { validate: () => boolean }).validate()) {
+  addSoftwareUpdateAction () {
+    if (!this.isLoggedIn) {
       return
     }
-    if (!(this.$refs.softwareTypeForm as Vue & { validate: () => boolean }).validate()) {
+    if (!this.softwareUpdateAction) {
       return
     }
-    if (!(this.$refs.contactForm as Vue & { validate: () => boolean}).validate()) {
+    if (!(this.$refs.softwareUpdateActionForm as Vue & { isValid: () => boolean }).isValid()) {
+      this.isSaving = false
+      this.$store.commit('snackbar/setError', 'Please correct the errors')
       return
     }
-    this.$store.commit('snackbar/setError', 'Not implemented yet')
+    this.isSaving = true
+    this.$api.deviceSoftwareUpdateActions.add(this.deviceId, this.softwareUpdateAction).then((action: SoftwareUpdateAction) => {
+      this.$router.push('/devices/' + this.deviceId + '/actions', () => this.$emit('input', action))
+    }).catch(() => {
+      this.$store.commit('snackbar/setError', 'Failed to save the action')
+    }).finally(() => {
+      this.isSaving = false
+    })
   }
 
   addGenericAction () {
diff --git a/pages/devices/_deviceId/actions/software-update-actions/_actionId/edit.vue b/pages/devices/_deviceId/actions/software-update-actions/_actionId/edit.vue
new file mode 100644
index 000000000..86a5d0201
--- /dev/null
+++ b/pages/devices/_deviceId/actions/software-update-actions/_actionId/edit.vue
@@ -0,0 +1,175 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020, 2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <div>
+    <v-card-actions>
+      <v-spacer />
+      <v-btn
+        v-if="isLoggedIn"
+        small
+        text
+        nuxt
+        :to="'/devices/' + deviceId + '/actions'"
+      >
+        cancel
+      </v-btn>
+      <v-btn
+        v-if="isLoggedIn"
+        color="green"
+        small
+        :disabled="isSaving"
+        @click="save"
+      >
+        apply
+      </v-btn>
+    </v-card-actions>
+
+    <SoftwareUpdateActionForm
+      ref="softwareUpdateActionForm"
+      v-model="action"
+      :attachments="attachments"
+    />
+
+    <v-card-actions>
+      <v-spacer />
+      <v-btn
+        v-if="isLoggedIn"
+        small
+        text
+        nuxt
+        :to="'/devices/' + deviceId + '/actions'"
+      >
+        cancel
+      </v-btn>
+      <v-btn
+        v-if="isLoggedIn"
+        color="green"
+        small
+        :disabled="isSaving"
+        @click="save"
+      >
+        apply
+      </v-btn>
+    </v-card-actions>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Vue } from 'nuxt-property-decorator'
+
+import { Attachment } from '@/models/Attachment'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
+
+import SoftwareUpdateActionForm from '@/components/SoftwareUpdateActionForm.vue'
+
+@Component({
+  components: {
+    SoftwareUpdateActionForm
+  },
+  scrollToTop: true
+})
+export default class DeviceSoftwareUpdateActionEditPage extends Vue {
+  private action: SoftwareUpdateAction = new SoftwareUpdateAction()
+  private attachments: Attachment[] = []
+  private _isLoading: boolean = false
+  private _isSaving: boolean = false
+
+  async fetch (): Promise<any> {
+    this.isLoading = true
+    await Promise.all([
+      this.fetchAttachments(),
+      this.fetchAction()
+    ])
+    this.isLoading = false
+  }
+
+  async fetchAction (): Promise<any> {
+    try {
+      this.action = await this.$api.deviceSoftwareUpdateActions.findById(this.actionId)
+    } catch (_) {
+      this.$store.commit('snackbar/setError', 'Failed to fetch action')
+    }
+  }
+
+  async fetchAttachments (): Promise<any> {
+    try {
+      this.attachments = await this.$api.devices.findRelatedDeviceAttachments(this.deviceId)
+    } catch (_) {
+      this.$store.commit('snackbar/setError', 'Failed to fetch attachments')
+    }
+  }
+
+  get deviceId (): string {
+    return this.$route.params.deviceId
+  }
+
+  get actionId (): string {
+    return this.$route.params.actionId
+  }
+
+  get isLoggedIn (): boolean {
+    return this.$store.getters['oidc/isAuthenticated']
+  }
+
+  get isLoading (): boolean {
+    return this.$data._isLoading
+  }
+
+  set isLoading (value: boolean) {
+    this.$data._isLoading = value
+    this.$emit('showload', value)
+  }
+
+  get isSaving (): boolean {
+    return this.$data._isSaving
+  }
+
+  set isSaving (value: boolean) {
+    this.$data._isSaving = value
+    this.$emit('showsave', value)
+  }
+
+  save (): void {
+    if (!(this.$refs.softwareUpdateActionForm as Vue & { isValid: () => boolean }).isValid()) {
+      this.$store.commit('snackbar/setError', 'Please correct the errors')
+      return
+    }
+    this.isSaving = true
+    this.$api.deviceSoftwareUpdateActions.update(this.deviceId, this.action).then((action: SoftwareUpdateAction) => {
+      this.$router.push('/devices/' + this.deviceId + '/actions', () => this.$emit('input', action))
+    }).catch(() => {
+      this.$store.commit('snackbar/setError', 'Failed to save the action')
+    }).finally(() => {
+      this.isSaving = false
+    })
+  }
+}
+</script>
diff --git a/serializers/jsonapi/ActionAttachmentSerializer.ts b/serializers/jsonapi/ActionAttachmentSerializer.ts
new file mode 100644
index 000000000..1edf47950
--- /dev/null
+++ b/serializers/jsonapi/ActionAttachmentSerializer.ts
@@ -0,0 +1,199 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { Attachment } from '@/models/Attachment'
+
+import {
+  IJsonApiEntityWithOptionalId,
+  IJsonApiEntityWithOptionalAttributes,
+  IJsonApiRelationships,
+  IJsonApiEntityWithoutDetails,
+  IJsonApiEntityWithoutDetailsDataDictList
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+import { IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer'
+import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer'
+import { PlatformAttachmentSerializer } from '@/serializers/jsonapi/PlatformAttachmentSerializer'
+
+export interface IActionAttachmentSerializer {
+  attachmentSerializer: IAttachmentSerializer
+  getActionTypeName (): string
+  getActionAttachmentTypeName (): string
+  getActionAttachmentTypeNamePlural (): string
+  getAttachmentTypeName (): string
+  convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId
+  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[]
+}
+
+export abstract class AbstractActionAttachmentSerializer implements IActionAttachmentSerializer {
+  abstract get attachmentSerializer (): IAttachmentSerializer
+  abstract getActionTypeName (): string
+  abstract getActionAttachmentTypeName (): string
+  abstract getActionAttachmentTypeNamePlural (): string
+  abstract getAttachmentTypeName (): string
+
+  convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId {
+    /**
+     * 2021-05-07 mha:
+     * We build the relation to the action by hand instead of using the
+     * GenericActionSerializer, to avoid circular references.  We also
+     * build the relation to the attachment by hand instead of using the
+     * DeviceAttachmentSerializer, which uses 'device_attachment' as its
+     * property whereas we need 'attachment' as the property and
+     * 'device_attachment' as the type.
+     */
+    const entityType = this.getActionAttachmentTypeName()
+    const actionType = this.getActionTypeName()
+    const attachmentType = this.getAttachmentTypeName()
+    const data: IJsonApiEntityWithOptionalId = {
+      type: entityType,
+      attributes: {},
+      relationships: {
+        action: {
+          data: {
+            type: actionType,
+            id: actionId
+          }
+        },
+        attachment: {
+          data: {
+            type: attachmentType,
+            id: attachment.id || ''
+          }
+        }
+      }
+    }
+    return data
+  }
+
+  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[] {
+    const actionAttachmentIds = []
+    const entityType = this.getActionAttachmentTypeName()
+    const typePlural = this.getActionAttachmentTypeNamePlural()
+    if (relationships[typePlural]) {
+      const attachmentObject = relationships[typePlural] as IJsonApiEntityWithoutDetailsDataDictList
+      if (attachmentObject.data && (attachmentObject.data as IJsonApiEntityWithoutDetails[]).length > 0) {
+        for (const relationShipAttachmentData of attachmentObject.data as IJsonApiEntityWithoutDetails[]) {
+          const actionAttachmentId = relationShipAttachmentData.id
+          actionAttachmentIds.push(actionAttachmentId)
+        }
+      }
+    }
+
+    const attachmentIds = []
+    if (included && included.length > 0) {
+      for (const includedEntry of included) {
+        if (includedEntry.type === entityType) {
+          const actionAttachmentId = includedEntry.id
+          if (actionAttachmentIds.includes(actionAttachmentId)) {
+            if ((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails | undefined)?.id) {
+              attachmentIds.push((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails).id)
+            }
+          }
+        }
+      }
+    }
+
+    const attachmentType = this.getAttachmentTypeName()
+    const attachments: Attachment[] = []
+    if (included && included.length > 0) {
+      for (const includedEntry of included) {
+        if (includedEntry.type === attachmentType) {
+          const attachmentId = includedEntry.id
+          if (attachmentIds.includes(attachmentId)) {
+            const attachment = this.attachmentSerializer.convertJsonApiDataToModel(includedEntry)
+            attachments.push(attachment)
+          }
+        }
+      }
+    }
+
+    return attachments
+  }
+}
+
+export class GenericDeviceActionAttachmentSerializer extends AbstractActionAttachmentSerializer {
+  private _attachmentSerializer: IAttachmentSerializer
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new DeviceAttachmentSerializer()
+  }
+
+  getActionTypeName (): string {
+    return 'generic_device_action'
+  }
+
+  getActionAttachmentTypeName (): string {
+    return 'generic_device_action_attachment'
+  }
+
+  getActionAttachmentTypeNamePlural (): string {
+    return this.getActionAttachmentTypeName() + 's'
+  }
+
+  getAttachmentTypeName (): string {
+    return 'device_attachment'
+  }
+
+  get attachmentSerializer (): IAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+}
+
+export class GenericPlatformActionAttachmentSerializer extends AbstractActionAttachmentSerializer {
+  private _attachmentSerializer: IAttachmentSerializer
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new PlatformAttachmentSerializer()
+  }
+
+  getActionTypeName (): string {
+    return 'generic_platform_action'
+  }
+
+  getActionAttachmentTypeName (): string {
+    return 'generic_platform_action_attachment'
+  }
+
+  getActionAttachmentTypeNamePlural (): string {
+    return this.getActionAttachmentTypeName() + 's'
+  }
+
+  getAttachmentTypeName (): string {
+    return 'platform_attachment'
+  }
+
+  get attachmentSerializer (): IAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+}
diff --git a/serializers/jsonapi/AttachmentSerializer.ts b/serializers/jsonapi/AttachmentSerializer.ts
index 0f757b7d3..549090cfe 100644
--- a/serializers/jsonapi/AttachmentSerializer.ts
+++ b/serializers/jsonapi/AttachmentSerializer.ts
@@ -53,7 +53,13 @@ export interface IAttachmentsAndMissing {
   missing: IMissingAttachmentData
 }
 
-export class AttachmentSerializer {
+export interface IAttachmentSerializer {
+  convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): Attachment
+  convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes): Attachment
+  convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): Attachment[]
+}
+
+export class AttachmentSerializer implements IAttachmentSerializer {
   convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): Attachment {
     const data = jsonApiObject.data
     return this.convertJsonApiDataToModel(data)
diff --git a/serializers/jsonapi/DeviceAttachmentSerializer.ts b/serializers/jsonapi/DeviceAttachmentSerializer.ts
index d63a2d7e6..3031abd28 100644
--- a/serializers/jsonapi/DeviceAttachmentSerializer.ts
+++ b/serializers/jsonapi/DeviceAttachmentSerializer.ts
@@ -43,9 +43,9 @@ import {
   IJsonApiEntityWithOptionalAttributes
 } from '@/serializers/jsonapi/JsonApiTypes'
 
-import { IAttachmentsAndMissing } from '@/serializers/jsonapi/AttachmentSerializer'
+import { IAttachmentsAndMissing, IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer'
 
-export class DeviceAttachmentSerializer {
+export class DeviceAttachmentSerializer implements IAttachmentSerializer {
   convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): Attachment {
     const data = jsonApiObject.data
     return this.convertJsonApiDataToModel(data)
diff --git a/serializers/jsonapi/GenericActionAttachmentSerializer.ts b/serializers/jsonapi/GenericActionAttachmentSerializer.ts
index 2500a9899..a65dd5516 100644
--- a/serializers/jsonapi/GenericActionAttachmentSerializer.ts
+++ b/serializers/jsonapi/GenericActionAttachmentSerializer.ts
@@ -29,118 +29,54 @@
  * implied. See the Licence for the specific language governing
  * permissions and limitations under the Licence.
  */
-import { Attachment } from '@/models/Attachment'
-
-import {
-  IJsonApiEntityWithOptionalId,
-  IJsonApiEntityWithOptionalAttributes,
-  IJsonApiRelationships,
-  IJsonApiEntityWithoutDetails,
-  IJsonApiEntityWithoutDetailsDataDictList
-} from '@/serializers/jsonapi/JsonApiTypes'
-
+import { IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer'
+import { AbstractActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer'
 import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer'
+import { PlatformAttachmentSerializer } from '@/serializers/jsonapi/PlatformAttachmentSerializer'
 
-export interface IGenericActionAttachmentSerializer {
-  targetType: string
-  convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId
-  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[]
-  getActionTypeName (): string
-  getActionAttachmentTypeName (): string
-  getActionAttachmentTypeNamePlural (): string
-  getAttachmentTypeName (): string
-}
+export class GenericDeviceActionAttachmentSerializer extends AbstractActionAttachmentSerializer {
+  private _attachmentSerializer: IAttachmentSerializer
 
-export abstract class AbstractGenericActionAttachmentSerializer implements IGenericActionAttachmentSerializer {
-  private attachmentSerializer: DeviceAttachmentSerializer = new DeviceAttachmentSerializer()
+  constructor () {
+    super()
+    this._attachmentSerializer = new DeviceAttachmentSerializer()
+  }
 
-  abstract get targetType (): string
+  getActionTypeName (): string {
+    return 'generic_device_action'
+  }
 
-  convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId {
-    /**
-     * 2021-05-07 mha:
-     * We build the relation to the action by hand instead of using the
-     * GenericActionSerializer, to avoid circular references.  We also
-     * build the relation to the attachment by hand instead of using the
-     * DeviceAttachmentSerializer, which uses 'device_attachment' as its
-     * property whereas we need 'attachment' as the property and
-     * 'device_attachment' as the type.
-     */
-    const entityType = this.getActionAttachmentTypeName()
-    const actionType = this.getActionTypeName()
-    const attachmentType = this.getAttachmentTypeName()
-    const data: IJsonApiEntityWithOptionalId = {
-      type: entityType,
-      attributes: {},
-      relationships: {
-        action: {
-          data: {
-            type: actionType,
-            id: actionId
-          }
-        },
-        attachment: {
-          data: {
-            type: attachmentType,
-            id: attachment.id || ''
-          }
-        }
-      }
-    }
-    return data
+  getActionAttachmentTypeName (): string {
+    return 'generic_device_action_attachment'
   }
 
-  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[] {
-    const actionAttachmentIds = []
-    const entityType = this.getActionAttachmentTypeName()
-    const typePlural = this.getActionAttachmentTypeNamePlural()
-    if (relationships[typePlural]) {
-      const attachmentObject = relationships[typePlural] as IJsonApiEntityWithoutDetailsDataDictList
-      if (attachmentObject.data && (attachmentObject.data as IJsonApiEntityWithoutDetails[]).length > 0) {
-        for (const relationShipAttachmentData of attachmentObject.data as IJsonApiEntityWithoutDetails[]) {
-          const actionAttachmentId = relationShipAttachmentData.id
-          actionAttachmentIds.push(actionAttachmentId)
-        }
-      }
-    }
+  getActionAttachmentTypeNamePlural (): string {
+    return this.getActionAttachmentTypeName() + 's'
+  }
 
-    const attachmentIds = []
-    if (included && included.length > 0) {
-      for (const includedEntry of included) {
-        if (includedEntry.type === entityType) {
-          const actionAttachmentId = includedEntry.id
-          if (actionAttachmentIds.includes(actionAttachmentId)) {
-            if ((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails | undefined)?.id) {
-              attachmentIds.push((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails).id)
-            }
-          }
-        }
-      }
-    }
+  getAttachmentTypeName (): string {
+    return 'device_attachment'
+  }
+
+  get attachmentSerializer (): IAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+}
 
-    const attachmentType = this.getAttachmentTypeName()
-    const attachments: Attachment[] = []
-    if (included && included.length > 0) {
-      for (const includedEntry of included) {
-        if (includedEntry.type === attachmentType) {
-          const attachmentId = includedEntry.id
-          if (attachmentIds.includes(attachmentId)) {
-            const attachment = this.attachmentSerializer.convertJsonApiDataToModel(includedEntry)
-            attachments.push(attachment)
-          }
-        }
-      }
-    }
+export class GenericPlatformActionAttachmentSerializer extends AbstractActionAttachmentSerializer {
+  private _attachmentSerializer: IAttachmentSerializer
 
-    return attachments
+  constructor () {
+    super()
+    this._attachmentSerializer = new PlatformAttachmentSerializer()
   }
 
   getActionTypeName (): string {
-    return 'generic_' + this.targetType + '_action'
+    return 'generic_platform_action'
   }
 
   getActionAttachmentTypeName (): string {
-    return 'generic_' + this.targetType + '_action_attachment'
+    return 'generic_platform_action_attachment'
   }
 
   getActionAttachmentTypeNamePlural (): string {
@@ -148,18 +84,10 @@ export abstract class AbstractGenericActionAttachmentSerializer implements IGene
   }
 
   getAttachmentTypeName (): string {
-    return this.targetType + '_attachment'
+    return 'platform_attachment'
   }
-}
-
-export class GenericDeviceActionAttachmentSerializer extends AbstractGenericActionAttachmentSerializer {
-  get targetType (): string {
-    return 'device'
-  }
-}
 
-export class GenericPlatformActionAttachmentSerializer extends AbstractGenericActionAttachmentSerializer {
-  get targetType (): string {
-    return 'platform'
+  get attachmentSerializer (): IAttachmentSerializer {
+    return this._attachmentSerializer
   }
 }
diff --git a/serializers/jsonapi/GenericActionSerializer.ts b/serializers/jsonapi/GenericActionSerializer.ts
index 2360bb662..3ded4a183 100644
--- a/serializers/jsonapi/GenericActionSerializer.ts
+++ b/serializers/jsonapi/GenericActionSerializer.ts
@@ -47,8 +47,9 @@ import {
   IContactAndMissing
 } from '@/serializers/jsonapi/ContactSerializer'
 
+import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer'
+
 import {
-  IGenericActionAttachmentSerializer,
   GenericDeviceActionAttachmentSerializer,
   GenericPlatformActionAttachmentSerializer
 } from '@/serializers/jsonapi/GenericActionAttachmentSerializer'
@@ -58,7 +59,7 @@ export interface IMissingGenericActionData {
 }
 
 export interface IGenericActionsAndMissing {
-  genericDeviceActions: GenericAction[]
+  genericActions: GenericAction[]
   missing: IMissingGenericActionData
 }
 
@@ -69,7 +70,7 @@ export interface IGenericActionAttachmentRelation {
 
 export interface IGenericActionSerializer {
   targetType: string
-  attachmentSerializer: IGenericActionAttachmentSerializer
+  attachmentSerializer: IActionAttachmentSerializer
   convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): GenericAction
   convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes, included: IJsonApiEntityWithOptionalAttributes[]): GenericAction
   convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): GenericAction[]
@@ -77,7 +78,7 @@ export interface IGenericActionSerializer {
   convertModelToJsonApiRelationshipObject (action: IGenericAction): IJsonApiRelationships
   convertModelToTupleWithIdAndType (action: IGenericAction): IJsonApiEntityWithoutDetails
   convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionsAndMissing
-  convertJsonApiIncludedGenericActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[]
+  convertJsonApiIncludedActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[]
   getActionTypeName (): string
   getActionTypeNamePlural (): string
   getActionAttachmentTypeName (): string
@@ -87,7 +88,7 @@ export abstract class AbstractGenericActionSerializer implements IGenericActionS
   private contactSerializer: ContactSerializer = new ContactSerializer()
 
   abstract get targetType (): string
-  abstract get attachmentSerializer (): IGenericActionAttachmentSerializer
+  abstract get attachmentSerializer (): IActionAttachmentSerializer
 
   convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): GenericAction {
     const data = jsonApiObject.data
@@ -218,14 +219,14 @@ export abstract class AbstractGenericActionSerializer implements IGenericActionS
     }
 
     return {
-      genericDeviceActions: actions,
+      genericActions: actions,
       missing: {
         ids: missingDataForActionIds
       }
     }
   }
 
-  convertJsonApiIncludedGenericActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[] {
+  convertJsonApiIncludedActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[] {
     const linkedAttachments: IGenericActionAttachmentRelation[] = []
     const type = this.getActionAttachmentTypeName()
     included.forEach((i) => {
@@ -261,35 +262,35 @@ export abstract class AbstractGenericActionSerializer implements IGenericActionS
 }
 
 export class GenericDeviceActionSerializer extends AbstractGenericActionSerializer {
-  private _attachmentSerializer: IGenericActionAttachmentSerializer
+  private _attachmentSerializer: IActionAttachmentSerializer
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new GenericDeviceActionAttachmentSerializer()
+  }
 
   get targetType (): string {
     return 'device'
   }
 
-  get attachmentSerializer (): IGenericActionAttachmentSerializer {
+  get attachmentSerializer (): IActionAttachmentSerializer {
     return this._attachmentSerializer
   }
+}
+
+export class GenericPlatformActionSerializer extends AbstractGenericActionSerializer {
+  private _attachmentSerializer: IActionAttachmentSerializer
 
   constructor () {
     super()
-    this._attachmentSerializer = new GenericDeviceActionAttachmentSerializer()
+    this._attachmentSerializer = new GenericPlatformActionAttachmentSerializer()
   }
-}
-
-export class GenericPlatformActionSerializer extends AbstractGenericActionSerializer {
-  private _attachmentSerializer: IGenericActionAttachmentSerializer
 
   get targetType (): string {
     return 'platform'
   }
 
-  get attachmentSerializer (): IGenericActionAttachmentSerializer {
+  get attachmentSerializer (): IActionAttachmentSerializer {
     return this._attachmentSerializer
   }
-
-  constructor () {
-    super()
-    this._attachmentSerializer = new GenericPlatformActionAttachmentSerializer()
-  }
 }
diff --git a/serializers/jsonapi/PlatformAttachmentSerializer.ts b/serializers/jsonapi/PlatformAttachmentSerializer.ts
index fc369b968..b4574abe4 100644
--- a/serializers/jsonapi/PlatformAttachmentSerializer.ts
+++ b/serializers/jsonapi/PlatformAttachmentSerializer.ts
@@ -43,9 +43,9 @@ import {
   IJsonApiTypedEntityWithoutDetailsDataDictList,
   IJsonApiRelationships
 } from '@/serializers/jsonapi/JsonApiTypes'
-import { IAttachmentsAndMissing } from '@/serializers/jsonapi/AttachmentSerializer'
+import { IAttachmentsAndMissing, IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer'
 
-export class PlatformAttachmentSerializer {
+export class PlatformAttachmentSerializer implements IAttachmentSerializer {
   convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): Attachment {
     const data = jsonApiObject.data
     return this.convertJsonApiDataToModel(data)
diff --git a/models/Action.ts b/serializers/jsonapi/SoftwareTypeSerializer.ts
similarity index 64%
rename from models/Action.ts
rename to serializers/jsonapi/SoftwareTypeSerializer.ts
index 4d90f3285..91ebb98a8 100644
--- a/models/Action.ts
+++ b/serializers/jsonapi/SoftwareTypeSerializer.ts
@@ -29,10 +29,24 @@
  * implied. See the Licence for the specific language governing
  * permissions and limitations under the Licence.
  */
-import { Contact } from '@/models/Contact'
+import { SoftwareType } from '@/models/SoftwareType'
 
-export interface IAction {
-  id: string | null
-  description: string
-  contact: Contact | null
+import {
+  IJsonApiEntityListEnvelope,
+  IJsonApiEntity
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+export class SoftwareTypeSerializer {
+  convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): SoftwareType[] {
+    return jsonApiObjectList.data.map(this.convertJsonApiDataToModel.bind(this))
+  }
+
+  convertJsonApiDataToModel (jsonApiData: IJsonApiEntity): SoftwareType {
+    const id = jsonApiData.id.toString()
+    const name = jsonApiData.attributes.term
+    const url = jsonApiData.links?.self || ''
+    const definition = jsonApiData.attributes.definition
+
+    return SoftwareType.createWithData(id, name, url, definition)
+  }
 }
diff --git a/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.ts b/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.ts
new file mode 100644
index 000000000..b5a156c8a
--- /dev/null
+++ b/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.ts
@@ -0,0 +1,93 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { IAttachmentSerializer } from '@/serializers/jsonapi/AttachmentSerializer'
+import { AbstractActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer'
+import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer'
+import { PlatformAttachmentSerializer } from '@/serializers/jsonapi/PlatformAttachmentSerializer'
+
+export class DeviceSoftwareUpdateActionAttachmentSerializer extends AbstractActionAttachmentSerializer {
+  private _attachmentSerializer: IAttachmentSerializer
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new DeviceAttachmentSerializer()
+  }
+
+  getActionTypeName (): string {
+    return 'device_software_update_action'
+  }
+
+  getActionAttachmentTypeName (): string {
+    return 'device_software_update_action_attachment'
+  }
+
+  getActionAttachmentTypeNamePlural (): string {
+    return this.getActionAttachmentTypeName() + 's'
+  }
+
+  getAttachmentTypeName (): string {
+    return 'device_attachment'
+  }
+
+  get attachmentSerializer (): IAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+}
+
+export class PlatformSoftwareUpdateActionAttachmentSerializer extends AbstractActionAttachmentSerializer {
+  private _attachmentSerializer: IAttachmentSerializer
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new PlatformAttachmentSerializer()
+  }
+
+  getActionTypeName (): string {
+    return 'platform_software_update_action'
+  }
+
+  getActionAttachmentTypeName (): string {
+    return 'platform_software_update_action_attachment'
+  }
+
+  getActionAttachmentTypeNamePlural (): string {
+    return this.getActionAttachmentTypeName() + 's'
+  }
+
+  getAttachmentTypeName (): string {
+    return 'platform_attachment'
+  }
+
+  get attachmentSerializer (): IAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+}
diff --git a/serializers/jsonapi/SoftwareUpdateActionSerializer.ts b/serializers/jsonapi/SoftwareUpdateActionSerializer.ts
new file mode 100644
index 000000000..e2202479c
--- /dev/null
+++ b/serializers/jsonapi/SoftwareUpdateActionSerializer.ts
@@ -0,0 +1,298 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+import { SoftwareUpdateAction, ISoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
+import { Attachment } from '@/models/Attachment'
+import {
+  IJsonApiEntityEnvelope,
+  IJsonApiEntityListEnvelope,
+  IJsonApiEntityWithOptionalId,
+  IJsonApiEntityWithOptionalAttributes,
+  IJsonApiEntityWithoutDetails,
+  IJsonApiEntityWithoutDetailsDataDictList,
+  IJsonApiRelationships
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+import {
+  ContactSerializer,
+  IContactAndMissing
+} from '@/serializers/jsonapi/ContactSerializer'
+
+import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer'
+
+import {
+  DeviceSoftwareUpdateActionAttachmentSerializer,
+  PlatformSoftwareUpdateActionAttachmentSerializer
+} from '@/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer'
+
+export interface IMissingSoftwareUpdateActionData {
+  ids: string[]
+}
+
+export interface ISoftwareUpdateActionsAndMissing {
+  softwareUpdateActions: SoftwareUpdateAction[]
+  missing: IMissingSoftwareUpdateActionData
+}
+
+export interface ISoftwareUpdateActionAttachmentRelation {
+  softwareUpdateActionAttachmentId: string
+  attachmentId: string
+}
+
+export interface ISoftwareUpdateActionSerializer {
+  targetType: string
+  attachmentSerializer: IActionAttachmentSerializer
+  convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): SoftwareUpdateAction
+  convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes, included: IJsonApiEntityWithOptionalAttributes[]): SoftwareUpdateAction
+  convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): SoftwareUpdateAction[]
+  convertModelToJsonApiData (action: SoftwareUpdateAction, deviceOrPlatformId: string): IJsonApiEntityWithOptionalId
+  convertModelToJsonApiRelationshipObject (action: ISoftwareUpdateAction): IJsonApiRelationships
+  convertModelToTupleWithIdAndType (action: ISoftwareUpdateAction): IJsonApiEntityWithoutDetails
+  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): ISoftwareUpdateActionsAndMissing
+  convertJsonApiIncludedActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): ISoftwareUpdateActionAttachmentRelation[]
+  getActionTypeName (): string
+  getActionTypeNamePlural (): string
+  getActionAttachmentTypeName (): string
+}
+
+export abstract class AbstractSoftwareUpdateActionSerializer implements ISoftwareUpdateActionSerializer {
+  private contactSerializer: ContactSerializer = new ContactSerializer()
+
+  abstract get targetType (): string
+  abstract get attachmentSerializer (): IActionAttachmentSerializer
+
+  convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): SoftwareUpdateAction {
+    const data = jsonApiObject.data
+    const included = jsonApiObject.included || []
+    return this.convertJsonApiDataToModel(data, included)
+  }
+
+  convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes, included: IJsonApiEntityWithOptionalAttributes[]): SoftwareUpdateAction {
+    const attributes = jsonApiData.attributes
+    const newEntry = SoftwareUpdateAction.createEmpty()
+
+    newEntry.id = jsonApiData.id.toString()
+    if (attributes) {
+      newEntry.description = attributes.description || ''
+      newEntry.softwareTypeName = attributes.software_type_name || ''
+      newEntry.softwareTypeUrl = attributes.software_type_uri || ''
+      newEntry.updateDate = attributes.update_date ? DateTime.fromISO(attributes.update_date, { zone: 'UTC' }) : null
+      newEntry.version = attributes.version || ''
+      newEntry.repositoryUrl = attributes.repository_url || ''
+    }
+
+    const relationships = jsonApiData.relationships || {}
+
+    const contactWithMissing: IContactAndMissing = this.contactSerializer.convertJsonApiRelationshipsSingleModel(relationships, included)
+    if (contactWithMissing.contact) {
+      newEntry.contact = contactWithMissing.contact
+    }
+
+    const attachments: Attachment[] = this.attachmentSerializer.convertJsonApiRelationshipsModelList(relationships, included)
+    if (attachments.length) {
+      newEntry.attachments = attachments
+    }
+
+    return newEntry
+  }
+
+  convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): SoftwareUpdateAction[] {
+    const included = jsonApiObjectList.included || []
+    return jsonApiObjectList.data.map((model) => {
+      return this.convertJsonApiDataToModel(model, included)
+    })
+  }
+
+  convertModelToJsonApiData (action: SoftwareUpdateAction, deviceOrPlatformId: string): IJsonApiEntityWithOptionalId {
+    const data: IJsonApiEntityWithOptionalId = {
+      type: this.getActionTypeName(),
+      attributes: {
+        description: action.description,
+        software_type_name: action.softwareTypeName,
+        software_type_uri: action.softwareTypeUrl,
+        update_date: action.updateDate != null ? action.updateDate.setZone('UTC').toISO() : null,
+        version: action.version,
+        repository_url: action.repositoryUrl
+      },
+      relationships: {
+        [this.targetType]: {
+          data: {
+            type: this.targetType,
+            id: deviceOrPlatformId
+          }
+        }
+      }
+    }
+    if (action.id) {
+      data.id = action.id
+    }
+    if (action.contact && action.contact.id) {
+      const contactRelationship = this.contactSerializer.convertModelToJsonApiRelationshipObject(action.contact)
+      data.relationships = {
+        ...data.relationships,
+        ...contactRelationship
+      }
+    }
+    // Note: Attachments are not included and must be send to the backend with
+    // a relation to the action after this action was saved
+    return data
+  }
+
+  convertModelToJsonApiRelationshipObject (action: ISoftwareUpdateAction): IJsonApiRelationships {
+    return {
+      [this.getActionTypeName()]: {
+        data: this.convertModelToTupleWithIdAndType(action)
+      }
+    }
+  }
+
+  convertModelToTupleWithIdAndType (action: ISoftwareUpdateAction): IJsonApiEntityWithoutDetails {
+    return {
+      id: action.id || '',
+      type: this.getActionTypeName()
+    }
+  }
+
+  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): ISoftwareUpdateActionsAndMissing {
+    const actionIds = []
+    const type = this.getActionTypeName()
+    const typePlural = this.getActionTypeNamePlural()
+    if (relationships[typePlural]) {
+      const actionObject = relationships[typePlural] as IJsonApiEntityWithoutDetailsDataDictList
+      if (actionObject.data && (actionObject.data as IJsonApiEntityWithoutDetails[]).length > 0) {
+        for (const relationShipActionData of actionObject.data) {
+          const actionId = relationShipActionData.id
+          actionIds.push(actionId)
+        }
+      }
+    }
+
+    const possibleActions: { [key: string]: SoftwareUpdateAction } = {}
+    if (included && included.length > 0) {
+      for (const includedEntry of included) {
+        if (includedEntry.type === type) {
+          const actionId = includedEntry.id
+          if (actionIds.includes(actionId)) {
+            const action = this.convertJsonApiDataToModel(includedEntry, [])
+            possibleActions[actionId] = action
+          }
+        }
+      }
+    }
+
+    const actions = []
+    const missingDataForActionIds = []
+
+    for (const actionId of actionIds) {
+      if (possibleActions[actionId]) {
+        actions.push(possibleActions[actionId])
+      } else {
+        missingDataForActionIds.push(actionId)
+      }
+    }
+
+    return {
+      softwareUpdateActions: actions,
+      missing: {
+        ids: missingDataForActionIds
+      }
+    }
+  }
+
+  convertJsonApiIncludedActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): ISoftwareUpdateActionAttachmentRelation[] {
+    const linkedAttachments: ISoftwareUpdateActionAttachmentRelation[] = []
+    const type = this.getActionAttachmentTypeName()
+    included.forEach((i) => {
+      if (!i.id) {
+        return
+      }
+      if (i.type !== type) {
+        return
+      }
+      if (!i.relationships?.attachment || !i.relationships?.attachment.data || !(i.relationships?.attachment.data as IJsonApiEntityWithoutDetails).id) {
+        return
+      }
+      const attachmentId: string = (i.relationships.attachment.data as IJsonApiEntityWithoutDetails).id
+      linkedAttachments.push({
+        softwareUpdateActionAttachmentId: i.id,
+        attachmentId
+      })
+    })
+    return linkedAttachments
+  }
+
+  getActionTypeName (): string {
+    return this.targetType + '_software_update_action'
+  }
+
+  getActionTypeNamePlural (): string {
+    return this.getActionTypeName() + 's'
+  }
+
+  getActionAttachmentTypeName (): string {
+    return this.targetType + '_software_update_action_attachment'
+  }
+}
+
+export class DeviceSoftwareUpdateActionSerializer extends AbstractSoftwareUpdateActionSerializer {
+  private _attachmentSerializer: IActionAttachmentSerializer
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+  }
+
+  get targetType (): string {
+    return 'device'
+  }
+
+  get attachmentSerializer (): IActionAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+}
+
+export class PlatformSoftwareUpdateActionSerializer extends AbstractSoftwareUpdateActionSerializer {
+  private _attachmentSerializer: IActionAttachmentSerializer
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new PlatformSoftwareUpdateActionAttachmentSerializer()
+  }
+
+  get targetType (): string {
+    return 'platform'
+  }
+
+  get attachmentSerializer (): IActionAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+}
diff --git a/services/Api.ts b/services/Api.ts
index 551e38f1a..bb33f4a47 100644
--- a/services/Api.ts
+++ b/services/Api.ts
@@ -41,7 +41,10 @@ import { CustomfieldsApi } from '@/services/sms/CustomfieldsApi'
 import { DeviceAttachmentApi } from '@/services/sms/DeviceAttachmentApi'
 import { PlatformAttachmentApi } from '@/services/sms/PlatformAttachmentApi'
 import { GenericDeviceActionApi } from '@/services/sms/GenericDeviceActionApi'
-import { GenericDeviceActionAttachmentApi } from '@/services/sms/GenericDeviceActionAttachmentApi'
+import { GenericDeviceActionAttachmentApi, GenericPlatformActionAttachmentApi } from '@/services/sms/GenericActionAttachmentApi'
+import { DeviceSoftwareUpdateActionApi } from '@/services/sms/DeviceSoftwareUpdateActionApi'
+import { PlatformSoftwareUpdateActionApi } from '@/services/sms/PlatformSoftwareUpdateActionApi'
+import { DeviceSoftwareUpdateActionAttachmentApi, PlatformSoftwareUpdateActionAttachmentApi } from '@/services/sms/SoftwareUpdateActionAttachmentApi'
 
 import { CompartmentApi } from '@/services/cv/CompartmentApi'
 import { DeviceTypeApi } from '@/services/cv/DeviceTypeApi'
@@ -53,6 +56,7 @@ import { StatusApi } from '@/services/cv/StatusApi'
 import { UnitApi } from '@/services/cv/UnitApi'
 import { MeasuredQuantityUnitApi } from '@/services/cv/MeasuredQuantityUnitApi'
 import { ActionTypeApi } from '@/services/cv/ActionTypeApi'
+import { SoftwareTypeApi } from '@/services/cv/SoftwareTypeApi'
 
 import { ProjectApi } from '@/services/project/ProjectApi'
 
@@ -71,6 +75,11 @@ export class Api {
   private readonly _devicePropertyApi: DevicePropertyApi
   private readonly _genericDeviceActionApi: GenericDeviceActionApi
   private readonly _genericDeviceActionAttachmentApi: GenericDeviceActionAttachmentApi
+  private readonly _genericPlatformActionAttachmentApi: GenericPlatformActionAttachmentApi
+  private readonly _deviceSoftwareUpdateActionApi: DeviceSoftwareUpdateActionApi
+  private readonly _deviceSoftwareUpdateActionAttachmentApi: DeviceSoftwareUpdateActionAttachmentApi
+  private readonly _platformSoftwareUpdateActionApi: PlatformSoftwareUpdateActionApi
+  private readonly _platformSoftwareUpdateActionAttachmentApi: PlatformSoftwareUpdateActionAttachmentApi
 
   private readonly _manufacturerApi: ManufacturerApi
   private readonly _platformTypeApi: PlatformTypeApi
@@ -82,6 +91,7 @@ export class Api {
   private readonly _unitApi: UnitApi
   private readonly _measuredQuantityUnitApi: MeasuredQuantityUnitApi
   private readonly _actionTypeApi: ActionTypeApi
+  private readonly _softwareTypeApi: SoftwareTypeApi
 
   private readonly _projectApi: ProjectApi
 
@@ -138,6 +148,28 @@ export class Api {
       this._genericDeviceActionAttachmentApi
     )
 
+    this._genericPlatformActionAttachmentApi = new GenericPlatformActionAttachmentApi(
+      this.createAxios(smsBaseUrl, '/generic-platform-action-attachments', smsConfig, getIdToken)
+    )
+
+    this._deviceSoftwareUpdateActionAttachmentApi = new DeviceSoftwareUpdateActionAttachmentApi(
+      this.createAxios(smsBaseUrl, '/device-software-update-action-attachments', smsConfig, getIdToken)
+    )
+
+    this._deviceSoftwareUpdateActionApi = new DeviceSoftwareUpdateActionApi(
+      this.createAxios(smsBaseUrl, '/device-software-update-actions', smsConfig, getIdToken),
+      this._deviceSoftwareUpdateActionAttachmentApi
+    )
+
+    this._platformSoftwareUpdateActionAttachmentApi = new PlatformSoftwareUpdateActionAttachmentApi(
+      this.createAxios(smsBaseUrl, '/platform-software-update-action-attachments', smsConfig, getIdToken)
+    )
+
+    this._platformSoftwareUpdateActionApi = new PlatformSoftwareUpdateActionApi(
+      this.createAxios(smsBaseUrl, '/platform-software-update-actions', smsConfig, getIdToken),
+      this._platformSoftwareUpdateActionAttachmentApi
+    )
+
     // and here we can set settings for all the cv api calls
     const cvConfig: AxiosRequestConfig = {
       headers: {
@@ -177,6 +209,9 @@ export class Api {
     this._actionTypeApi = new ActionTypeApi(
       this.createAxios(cvBaseUrl, '/actiontypes/', cvConfig)
     )
+    this._softwareTypeApi = new SoftwareTypeApi(
+      this.createAxios(cvBaseUrl, '/softwaretypes/', cvConfig)
+    )
 
     this._projectApi = new ProjectApi()
   }
@@ -245,6 +280,22 @@ export class Api {
     return this._genericDeviceActionAttachmentApi
   }
 
+  get deviceSoftwareUpdateActions (): DeviceSoftwareUpdateActionApi {
+    return this._deviceSoftwareUpdateActionApi
+  }
+
+  get deviceSoftwareUpdateActionAttachments (): DeviceSoftwareUpdateActionAttachmentApi {
+    return this._deviceSoftwareUpdateActionAttachmentApi
+  }
+
+  get platformSoftwareUpdateActions (): PlatformSoftwareUpdateActionApi {
+    return this._platformSoftwareUpdateActionApi
+  }
+
+  get platformSoftwareUpdateActionAttachments (): PlatformSoftwareUpdateActionAttachmentApi {
+    return this._platformSoftwareUpdateActionAttachmentApi
+  }
+
   get contacts (): ContactApi {
     return this._contactApi
   }
@@ -289,6 +340,10 @@ export class Api {
     return this._actionTypeApi
   }
 
+  get softwareTypes (): SoftwareTypeApi {
+    return this._softwareTypeApi
+  }
+
   get projects (): ProjectApi {
     return this._projectApi
   }
diff --git a/services/cv/SoftwareTypeApi.ts b/services/cv/SoftwareTypeApi.ts
new file mode 100644
index 000000000..769729a8b
--- /dev/null
+++ b/services/cv/SoftwareTypeApi.ts
@@ -0,0 +1,155 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { AxiosInstance } from 'axios'
+
+import { SoftwareType } from '@/models/SoftwareType'
+import { SoftwareTypeSerializer } from '@/serializers/jsonapi/SoftwareTypeSerializer'
+import { CVApi } from '@/services/cv/CVApi'
+
+import { IPaginationLoader } from '@/utils/PaginatedLoader'
+
+export class SoftwareTypeApi extends CVApi<SoftwareType> {
+  private serializer: SoftwareTypeSerializer
+
+  constructor (axiosInstance: AxiosInstance) {
+    super(axiosInstance)
+    this.serializer = new SoftwareTypeSerializer()
+  }
+
+  newSearchBuilder (): SoftwareTypeSearchBuilder {
+    return new SoftwareTypeSearchBuilder(this.axiosApi, this.serializer)
+  }
+
+  findAll (): Promise<SoftwareType[]> {
+    return this.newSearchBuilder().build().findMatchingAsList().then(data => SoftwareTypeApi.sort(data))
+  }
+
+  findAllPaginated (pageSize: number = 100): Promise<SoftwareType[]> {
+    return this.newSearchBuilder().build().findMatchingAsPaginationLoader(pageSize).then(loader => this.loadPaginated(loader)).then(data => SoftwareTypeApi.sort(data))
+  }
+
+  static sort (softwareTypes: SoftwareType[]): SoftwareType[] {
+    const softwareTypesCopy: SoftwareType[] = [...softwareTypes]
+    // sort alphabetical
+    softwareTypesCopy.sort((a, b) => {
+      if (a.name < b.name) {
+        return -1
+      }
+      if (a.name > b.name) {
+        return 1
+      }
+      return 0
+    })
+    // move 'Others' to the end
+    const othersIndex: number = softwareTypesCopy.findIndex(i => i.name.toLowerCase() === 'others')
+    if (othersIndex > -1) {
+      const other: SoftwareType = softwareTypesCopy.splice(othersIndex, 1)[0]
+      softwareTypesCopy.push(other)
+    }
+    return softwareTypesCopy
+  }
+}
+
+export class SoftwareTypeSearchBuilder {
+  private axiosApi: AxiosInstance
+  private serializer: SoftwareTypeSerializer
+
+  constructor (axiosApi: AxiosInstance, serializer: SoftwareTypeSerializer) {
+    this.axiosApi = axiosApi
+    this.serializer = serializer
+  }
+
+  build (): SoftwareTypeSearcher {
+    return new SoftwareTypeSearcher(this.axiosApi, this.serializer)
+  }
+}
+
+export class SoftwareTypeSearcher {
+  private axiosApi: AxiosInstance
+  private serializer: SoftwareTypeSerializer
+
+  constructor (axiosApi: AxiosInstance, serializer: SoftwareTypeSerializer) {
+    this.axiosApi = axiosApi
+    this.serializer = serializer
+  }
+
+  private findAllOnPage (page: number, pageSize: number): Promise<IPaginationLoader<SoftwareType>> {
+    const params: { [idx: string]: any } = {
+      'page[size]': pageSize,
+      'page[number]': page,
+      'filter[status.iexact]': 'ACCEPTED',
+      sort: 'term'
+    }
+    return this.axiosApi.get(
+      '',
+      {
+        params
+      }
+    ).then((rawResponse) => {
+      const response = rawResponse.data
+      const elements: SoftwareType[] = this.serializer.convertJsonApiObjectListToModelList(response)
+      const totalCount = response.meta.count
+
+      let funToLoadNext = null
+      if (response.meta.pagination.page < response.meta.pagination.pages) {
+        funToLoadNext = () => this.findAllOnPage(page + 1, pageSize)
+      }
+
+      return {
+        elements,
+        totalCount,
+        funToLoadNext
+      }
+    })
+  }
+
+  findMatchingAsList (): Promise<SoftwareType[]> {
+    const params: { [idx: string]: any } = {
+      'page[size]': 10000,
+      'filter[status.iexact]': 'ACCEPTED',
+      sort: 'term'
+    }
+    return this.axiosApi.get(
+      '',
+      {
+        params
+      }
+    ).then((rawResponse) => {
+      const response = rawResponse.data
+      return this.serializer.convertJsonApiObjectListToModelList(response)
+    })
+  }
+
+  findMatchingAsPaginationLoader (pageSize: number): Promise<IPaginationLoader<SoftwareType>> {
+    return this.findAllOnPage(1, pageSize)
+  }
+}
diff --git a/services/sms/GenericDeviceActionAttachmentApi.ts b/services/sms/ActionAttachmentApi.ts
similarity index 81%
rename from services/sms/GenericDeviceActionAttachmentApi.ts
rename to services/sms/ActionAttachmentApi.ts
index b812717b3..2792e80d7 100644
--- a/services/sms/GenericDeviceActionAttachmentApi.ts
+++ b/services/sms/ActionAttachmentApi.ts
@@ -32,15 +32,21 @@
 import { AxiosInstance } from 'axios'
 
 import { Attachment } from '@/models/Attachment'
-import { IGenericActionAttachmentSerializer, GenericDeviceActionAttachmentSerializer } from '@/serializers/jsonapi/GenericActionAttachmentSerializer'
+import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer'
 
-export class GenericDeviceActionAttachmentApi {
+export interface IActionAttachmentApi {
+  serializer: IActionAttachmentSerializer
+  add (actionId: string, attachment: Attachment): Promise<any>
+  delete (id: string): Promise<void>
+}
+
+export abstract class AbstractActionAttachmentApi implements IActionAttachmentApi {
   private axiosApi: AxiosInstance
-  private serializer: IGenericActionAttachmentSerializer
+
+  abstract get serializer (): IActionAttachmentSerializer
 
   constructor (axiosInstance: AxiosInstance) {
     this.axiosApi = axiosInstance
-    this.serializer = new GenericDeviceActionAttachmentSerializer()
   }
 
   async add (actionId: string, attachment: Attachment): Promise<any> {
diff --git a/services/sms/DeviceApi.ts b/services/sms/DeviceApi.ts
index 6447ea704..954c54902 100644
--- a/services/sms/DeviceApi.ts
+++ b/services/sms/DeviceApi.ts
@@ -40,12 +40,14 @@ import { DeviceType } from '@/models/DeviceType'
 import { Manufacturer } from '@/models/Manufacturer'
 import { Status } from '@/models/Status'
 import { GenericAction } from '@/models/GenericAction'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
 
 import { ContactSerializer } from '@/serializers/jsonapi/ContactSerializer'
 import { CustomTextFieldSerializer } from '@/serializers/jsonapi/CustomTextFieldSerializer'
 import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer'
 import { DevicePropertySerializer } from '@/serializers/jsonapi/DevicePropertySerializer'
 import { GenericDeviceActionSerializer } from '@/serializers/jsonapi/GenericActionSerializer'
+import { DeviceSoftwareUpdateActionSerializer } from '@/serializers/jsonapi/SoftwareUpdateActionSerializer'
 
 import { IFlaskJSONAPIFilter } from '@/utils/JSONApiInterfaces'
 
@@ -211,6 +213,20 @@ export class DeviceApi {
       return new GenericDeviceActionSerializer().convertJsonApiObjectListToModelList(rawServerResponse.data)
     })
   }
+
+  findRelatedSoftwareUpdateActions (deviceId: string): Promise<SoftwareUpdateAction[]> {
+    const url = deviceId + '/device-software-update-actions'
+    const params = {
+      'page[size]': 10000,
+      include: [
+        'contact',
+        'device_software_update_action_attachments.attachment'
+      ].join(',')
+    }
+    return this.axiosApi.get(url, { params }).then((rawServerResponse) => {
+      return new DeviceSoftwareUpdateActionSerializer().convertJsonApiObjectListToModelList(rawServerResponse.data)
+    })
+  }
 }
 
 export class DeviceSearchBuilder {
diff --git a/services/sms/DeviceSoftwareUpdateActionApi.ts b/services/sms/DeviceSoftwareUpdateActionApi.ts
new file mode 100644
index 000000000..a91647bf9
--- /dev/null
+++ b/services/sms/DeviceSoftwareUpdateActionApi.ts
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { AxiosInstance } from 'axios'
+
+import { Attachment } from '@/models/Attachment'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
+import { DeviceSoftwareUpdateActionAttachmentApi } from '@/services/sms/SoftwareUpdateActionAttachmentApi'
+import { ISoftwareUpdateActionSerializer, DeviceSoftwareUpdateActionSerializer } from '@/serializers/jsonapi/SoftwareUpdateActionSerializer'
+
+export class DeviceSoftwareUpdateActionApi {
+  private axiosApi: AxiosInstance
+  private serializer: ISoftwareUpdateActionSerializer
+  private attachmentApi: DeviceSoftwareUpdateActionAttachmentApi
+
+  constructor (axiosInstance: AxiosInstance, attachmentApi: DeviceSoftwareUpdateActionAttachmentApi) {
+    this.axiosApi = axiosInstance
+    this.serializer = new DeviceSoftwareUpdateActionSerializer()
+    this.attachmentApi = attachmentApi
+  }
+
+  async findById (id: string): Promise<SoftwareUpdateAction> {
+    const response = await this.axiosApi.get(id, {
+      params: {
+        include: [
+          'contact',
+          'device_software_update_action_attachments.attachment'
+        ].join(',')
+      }
+    })
+    const data = response.data
+    return this.serializer.convertJsonApiObjectToModel(data)
+  }
+
+  deleteById (id: string): Promise<void> {
+    return this.axiosApi.delete<string, void>(id)
+  }
+
+  async add (deviceId: string, action: SoftwareUpdateAction): Promise<SoftwareUpdateAction> {
+    const url = ''
+    const data = this.serializer.convertModelToJsonApiData(action, deviceId)
+    const response = await this.axiosApi.post(url, { data })
+    const savedAction = this.serializer.convertJsonApiObjectToModel(response.data)
+    // save every attachment as an ActionAttachment
+    if (savedAction.id) {
+      const promises = action.attachments.map((attachment: Attachment) => this.attachmentApi.add(savedAction.id as string, attachment))
+      await Promise.all(promises)
+    }
+    return savedAction
+  }
+
+  async update (deviceId: string, action: SoftwareUpdateAction): Promise<SoftwareUpdateAction> {
+    if (!action.id) {
+      throw new Error('no id for the SoftwareUpdateAction')
+    }
+    // load the stored action to get a list of the device action attachments before the update
+    const attRawResponse = await this.axiosApi.get(action.id, {
+      params: {
+        include: [
+          'device_software_update_action_attachments.attachment'
+        ].join(',')
+      }
+    })
+    const attResponseData = attRawResponse.data
+    const included = attResponseData.included
+
+    // get the relations between attachments and device action attachments
+    const linkedAttachments: { [attachmentId: string]: string } = {}
+    if (included) {
+      const relations = this.serializer.convertJsonApiIncludedActionAttachmentsToIdList(included)
+      // convert to object to gain faster access to its members
+      relations.forEach((rel) => {
+        linkedAttachments[rel.attachmentId] = rel.softwareUpdateActionAttachmentId
+      })
+    }
+
+    // update the action
+    const data = this.serializer.convertModelToJsonApiData(action, deviceId)
+    const actionResponse = await this.axiosApi.patch(action.id, { data })
+
+    // find new attachments
+    const newAttachments: Attachment[] = []
+    action.attachments.forEach((attachment: Attachment) => {
+      if (attachment.id && linkedAttachments[attachment.id]) {
+        return
+      }
+      newAttachments.push(attachment)
+    })
+
+    // find deleted attachments
+    const deviceActionAttachmentsToDelete: string[] = []
+    for (const attachmentId in linkedAttachments) {
+      if (action.attachments.find((i: Attachment) => i.id === attachmentId)) {
+        continue
+      }
+      deviceActionAttachmentsToDelete.push(linkedAttachments[attachmentId])
+    }
+
+    // when there are no new attachments, newPromises is empty, which is okay
+    const newPromises = newAttachments.map((attachment: Attachment) => this.attachmentApi.add(action.id as string, attachment))
+    // when there are no deleted attachments, deletedPromises is empty, which is okay
+    const deletedPromises = deviceActionAttachmentsToDelete.map((id: string) => this.attachmentApi.delete(id))
+    await Promise.all([...deletedPromises, ...newPromises])
+
+    return this.serializer.convertJsonApiObjectToModel(actionResponse.data)
+  }
+
+  findRelatedActionAttachments (actionId: string): Promise<SoftwareUpdateAction[]> {
+    const url = actionId + '/device-software-update-action-attachments'
+    const params = {
+      'page[size]': 10000,
+      include: 'attachment'
+    }
+    return this.axiosApi.get(url, { params }).then((rawServerResponse) => {
+      return this.serializer.convertJsonApiObjectListToModelList(rawServerResponse.data)
+    })
+  }
+}
diff --git a/services/sms/GenericActionAttachmentApi.ts b/services/sms/GenericActionAttachmentApi.ts
new file mode 100644
index 000000000..bbb17b285
--- /dev/null
+++ b/services/sms/GenericActionAttachmentApi.ts
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { AxiosInstance } from 'axios'
+
+import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer'
+import { GenericDeviceActionAttachmentSerializer, GenericPlatformActionAttachmentSerializer } from '@/serializers/jsonapi/GenericActionAttachmentSerializer'
+import { AbstractActionAttachmentApi } from '@/services/sms/ActionAttachmentApi'
+
+export class GenericDeviceActionAttachmentApi extends AbstractActionAttachmentApi {
+  private _serializer: IActionAttachmentSerializer
+
+  constructor (axiosInstance: AxiosInstance) {
+    super(axiosInstance)
+    this._serializer = new GenericDeviceActionAttachmentSerializer()
+  }
+
+  get serializer (): IActionAttachmentSerializer {
+    return this._serializer
+  }
+}
+
+export class GenericPlatformActionAttachmentApi extends AbstractActionAttachmentApi {
+  private _serializer: IActionAttachmentSerializer
+
+  constructor (axiosInstance: AxiosInstance) {
+    super(axiosInstance)
+    this._serializer = new GenericPlatformActionAttachmentSerializer()
+  }
+
+  get serializer (): IActionAttachmentSerializer {
+    return this._serializer
+  }
+}
diff --git a/services/sms/GenericDeviceActionApi.ts b/services/sms/GenericDeviceActionApi.ts
index 4faf5eb06..9a8a6724e 100644
--- a/services/sms/GenericDeviceActionApi.ts
+++ b/services/sms/GenericDeviceActionApi.ts
@@ -33,7 +33,7 @@ import { AxiosInstance } from 'axios'
 
 import { Attachment } from '@/models/Attachment'
 import { GenericAction } from '@/models/GenericAction'
-import { GenericDeviceActionAttachmentApi } from '@/services/sms/GenericDeviceActionAttachmentApi'
+import { GenericDeviceActionAttachmentApi } from '@/services/sms/GenericActionAttachmentApi'
 import { IGenericActionSerializer, GenericDeviceActionSerializer } from '@/serializers/jsonapi/GenericActionSerializer'
 
 export class GenericDeviceActionApi {
@@ -95,7 +95,7 @@ export class GenericDeviceActionApi {
     // get the relations between attachments and generic device action attachments
     const linkedAttachments: { [attachmentId: string]: string } = {}
     if (included) {
-      const relations = this.serializer.convertJsonApiIncludedGenericActionAttachmentsToIdList(included)
+      const relations = this.serializer.convertJsonApiIncludedActionAttachmentsToIdList(included)
       // convert to object to gain faster access to its members
       relations.forEach((rel) => {
         linkedAttachments[rel.attachmentId] = rel.genericActionAttachmentId
diff --git a/services/sms/PlatformSoftwareUpdateActionApi.ts b/services/sms/PlatformSoftwareUpdateActionApi.ts
new file mode 100644
index 000000000..81b5b4fe7
--- /dev/null
+++ b/services/sms/PlatformSoftwareUpdateActionApi.ts
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { AxiosInstance } from 'axios'
+
+import { Attachment } from '@/models/Attachment'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
+import { PlatformSoftwareUpdateActionAttachmentApi } from '@/services/sms/SoftwareUpdateActionAttachmentApi'
+import { ISoftwareUpdateActionSerializer, PlatformSoftwareUpdateActionSerializer } from '@/serializers/jsonapi/SoftwareUpdateActionSerializer'
+
+export class PlatformSoftwareUpdateActionApi {
+  private axiosApi: AxiosInstance
+  private serializer: ISoftwareUpdateActionSerializer
+  private attachmentApi: PlatformSoftwareUpdateActionAttachmentApi
+
+  constructor (axiosInstance: AxiosInstance, attachmentApi: PlatformSoftwareUpdateActionAttachmentApi) {
+    this.axiosApi = axiosInstance
+    this.serializer = new PlatformSoftwareUpdateActionSerializer()
+    this.attachmentApi = attachmentApi
+  }
+
+  async findById (id: string): Promise<SoftwareUpdateAction> {
+    const response = await this.axiosApi.get(id, {
+      params: {
+        include: [
+          'contact',
+          'platform_software_update_action_attachments.attachment'
+        ].join(',')
+      }
+    })
+    const data = response.data
+    return this.serializer.convertJsonApiObjectToModel(data)
+  }
+
+  deleteById (id: string): Promise<void> {
+    return this.axiosApi.delete<string, void>(id)
+  }
+
+  async add (platformId: string, action: SoftwareUpdateAction): Promise<SoftwareUpdateAction> {
+    const url = ''
+    const data = this.serializer.convertModelToJsonApiData(action, platformId)
+    const response = await this.axiosApi.post(url, { data })
+    const savedAction = this.serializer.convertJsonApiObjectToModel(response.data)
+    // save every attachment as an ActionAttachment
+    if (savedAction.id) {
+      const promises = action.attachments.map((attachment: Attachment) => this.attachmentApi.add(savedAction.id as string, attachment))
+      await Promise.all(promises)
+    }
+    return savedAction
+  }
+
+  async update (platformId: string, action: SoftwareUpdateAction): Promise<SoftwareUpdateAction> {
+    if (!action.id) {
+      throw new Error('no id for the SoftwareUpdateAction')
+    }
+    // load the stored action to get a list of the platform action attachments before the update
+    const attRawResponse = await this.axiosApi.get(action.id, {
+      params: {
+        include: [
+          'platform_software_update_action_attachments.attachment'
+        ].join(',')
+      }
+    })
+    const attResponseData = attRawResponse.data
+    const included = attResponseData.included
+
+    // get the relations between attachments and platform action attachments
+    const linkedAttachments: { [attachmentId: string]: string } = {}
+    if (included) {
+      const relations = this.serializer.convertJsonApiIncludedActionAttachmentsToIdList(included)
+      // convert to object to gain faster access to its members
+      relations.forEach((rel) => {
+        linkedAttachments[rel.attachmentId] = rel.softwareUpdateActionAttachmentId
+      })
+    }
+
+    // update the action
+    const data = this.serializer.convertModelToJsonApiData(action, platformId)
+    const actionResponse = await this.axiosApi.patch(action.id, { data })
+
+    // find new attachments
+    const newAttachments: Attachment[] = []
+    action.attachments.forEach((attachment: Attachment) => {
+      if (attachment.id && linkedAttachments[attachment.id]) {
+        return
+      }
+      newAttachments.push(attachment)
+    })
+
+    // find deleted attachments
+    const platformActionAttachmentsToDelete: string[] = []
+    for (const attachmentId in linkedAttachments) {
+      if (action.attachments.find((i: Attachment) => i.id === attachmentId)) {
+        continue
+      }
+      platformActionAttachmentsToDelete.push(linkedAttachments[attachmentId])
+    }
+
+    // when there are no new attachments, newPromises is empty, which is okay
+    const newPromises = newAttachments.map((attachment: Attachment) => this.attachmentApi.add(action.id as string, attachment))
+    // when there are no deleted attachments, deletedPromises is empty, which is okay
+    const deletedPromises = platformActionAttachmentsToDelete.map((id: string) => this.attachmentApi.delete(id))
+    await Promise.all([...deletedPromises, ...newPromises])
+
+    return this.serializer.convertJsonApiObjectToModel(actionResponse.data)
+  }
+
+  findRelatedActionAttachments (actionId: string): Promise<SoftwareUpdateAction[]> {
+    const url = actionId + '/platform-software-update-action-attachments'
+    const params = {
+      'page[size]': 10000,
+      include: 'attachment'
+    }
+    return this.axiosApi.get(url, { params }).then((rawServerResponse) => {
+      return this.serializer.convertJsonApiObjectListToModelList(rawServerResponse.data)
+    })
+  }
+}
diff --git a/services/sms/SoftwareUpdateActionAttachmentApi.ts b/services/sms/SoftwareUpdateActionAttachmentApi.ts
new file mode 100644
index 000000000..bf9e1347d
--- /dev/null
+++ b/services/sms/SoftwareUpdateActionAttachmentApi.ts
@@ -0,0 +1,62 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { AxiosInstance } from 'axios'
+
+import { IActionAttachmentSerializer } from '@/serializers/jsonapi/ActionAttachmentSerializer'
+import { DeviceSoftwareUpdateActionAttachmentSerializer, PlatformSoftwareUpdateActionAttachmentSerializer } from '@/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer'
+import { AbstractActionAttachmentApi } from '@/services/sms/ActionAttachmentApi'
+
+export class DeviceSoftwareUpdateActionAttachmentApi extends AbstractActionAttachmentApi {
+  private _serializer: IActionAttachmentSerializer
+
+  constructor (axiosInstance: AxiosInstance) {
+    super(axiosInstance)
+    this._serializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+  }
+
+  get serializer (): IActionAttachmentSerializer {
+    return this._serializer
+  }
+}
+
+export class PlatformSoftwareUpdateActionAttachmentApi extends AbstractActionAttachmentApi {
+  private _serializer: IActionAttachmentSerializer
+
+  constructor (axiosInstance: AxiosInstance) {
+    super(axiosInstance)
+    this._serializer = new PlatformSoftwareUpdateActionAttachmentSerializer()
+  }
+
+  get serializer (): IActionAttachmentSerializer {
+    return this._serializer
+  }
+}
diff --git a/test/models/GenericAction.test.ts b/test/models/GenericAction.test.ts
index e54200bf2..ce47e0e4e 100644
--- a/test/models/GenericAction.test.ts
+++ b/test/models/GenericAction.test.ts
@@ -67,6 +67,7 @@ describe('GenericAction', () => {
     expect(action).toHaveProperty('actionTypeUrl', 'https://foo/bar')
     expect(action.beginDate).toBe(date1)
     expect(action.endDate).toBe(date2)
+    expect(action.date).toBe(date1)
     expect(action.contact).toStrictEqual(contact)
     expect(action.attachments).toContainEqual(attachment)
     expect(action.isGenericAction).toBeTruthy()
diff --git a/test/models/SoftwareUpdateAction.test.ts b/test/models/SoftwareUpdateAction.test.ts
new file mode 100644
index 000000000..8880bd80f
--- /dev/null
+++ b/test/models/SoftwareUpdateAction.test.ts
@@ -0,0 +1,76 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+import { Attachment } from '@/models/Attachment'
+import { Contact } from '@/models/Contact'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
+
+describe('SoftwareUpdateAction', () => {
+  test('create a SoftwareUpdateAction from an object', () => {
+    const date1 = DateTime.fromISO('2021-05-27')
+
+    const attachment = new Attachment()
+    attachment.id = '1'
+    attachment.label = 'an attachment'
+    attachment.url = 'https://foo/baz'
+
+    const contact = new Contact()
+    contact.givenName = 'Homer'
+    contact.familyName = 'Simpson'
+    contact.email = 'homer.simpson@springfield.com'
+
+    const action = SoftwareUpdateAction.createFromObject({
+      id: '1',
+      description: 'This is a software update action description',
+      softwareTypeName: 'Software Update',
+      softwareTypeUrl: 'https://foo/bar',
+      updateDate: date1,
+      version: '1.03',
+      repositoryUrl: 'https://git.gfz-potsdam.de/sensor-system-management/frontend',
+      contact,
+      attachments: [attachment]
+    })
+
+    expect(typeof action).toBe('object')
+    expect(action).toHaveProperty('id', '1')
+    expect(action).toHaveProperty('description', 'This is a software update action description')
+    expect(action).toHaveProperty('softwareTypeName', 'Software Update')
+    expect(action).toHaveProperty('softwareTypeUrl', 'https://foo/bar')
+    expect(action).toHaveProperty('version', '1.03')
+    expect(action).toHaveProperty('repositoryUrl', 'https://git.gfz-potsdam.de/sensor-system-management/frontend')
+    expect(action.updateDate).toBe(date1)
+    expect(action.date).toBe(date1)
+    expect(action.contact).toStrictEqual(contact)
+    expect(action.attachments).toContainEqual(attachment)
+    expect(action.isSoftwareUpdateAction).toBeTruthy()
+  })
+})
diff --git a/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts b/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts
index 951aeca4e..0fa289805 100644
--- a/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts
+++ b/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts
@@ -208,10 +208,6 @@ describe('GenericActionAttachmentSerializer', () => {
     }
 
     describe('constructing and types', () => {
-      it('should return \'device\' as its type', () => {
-        const serializer = new GenericDeviceActionAttachmentSerializer()
-        expect(serializer.targetType).toEqual('device')
-      })
       it('should return a correct action type name', () => {
         const serializer = new GenericDeviceActionAttachmentSerializer()
         expect(serializer.getActionTypeName()).toEqual('generic_device_action')
@@ -228,6 +224,10 @@ describe('GenericActionAttachmentSerializer', () => {
         const serializer = new GenericDeviceActionAttachmentSerializer()
         expect(serializer.getAttachmentTypeName()).toEqual('device_attachment')
       })
+      it('should return an attachment serializer', () => {
+        const serializer = new GenericDeviceActionAttachmentSerializer()
+        expect(typeof serializer.attachmentSerializer).toBe('object')
+      })
     })
     describe('#convertModelToJsonApiData', () => {
       it('should return a JSON API object from an attachment and an action id', () => {
@@ -289,10 +289,6 @@ describe('GenericActionAttachmentSerializer', () => {
   })
   describe('GenericPlatformActionAttachmentSerializer', () => {
     describe('constructing and types', () => {
-      it('should return \'platform\' as its type', () => {
-        const serializer = new GenericPlatformActionAttachmentSerializer()
-        expect(serializer.targetType).toEqual('platform')
-      })
       it('should return a correct action type name', () => {
         const serializer = new GenericPlatformActionAttachmentSerializer()
         expect(serializer.getActionTypeName()).toEqual('generic_platform_action')
@@ -309,6 +305,10 @@ describe('GenericActionAttachmentSerializer', () => {
         const serializer = new GenericPlatformActionAttachmentSerializer()
         expect(serializer.getAttachmentTypeName()).toEqual('platform_attachment')
       })
+      it('should return an attachment serializer', () => {
+        const serializer = new GenericPlatformActionAttachmentSerializer()
+        expect(typeof serializer.attachmentSerializer).toBe('object')
+      })
     })
   })
 })
diff --git a/test/serializers/jsonapi/GenericActionSerializer.test.ts b/test/serializers/jsonapi/GenericActionSerializer.test.ts
index 44c487d53..572d4b4be 100644
--- a/test/serializers/jsonapi/GenericActionSerializer.test.ts
+++ b/test/serializers/jsonapi/GenericActionSerializer.test.ts
@@ -787,9 +787,9 @@ describe('GenericActionSerializer', () => {
 
         const actionList = serializer.convertJsonApiRelationshipsModelList(relationships as IJsonApiRelationships, included as IJsonApiEntityWithOptionalAttributes[])
 
-        expect(actionList).toHaveProperty('genericDeviceActions')
-        expect(actionList.genericDeviceActions).toContainEqual(expectedAction1)
-        expect(actionList.genericDeviceActions).toContainEqual(expectedAction2)
+        expect(actionList).toHaveProperty('genericActions')
+        expect(actionList.genericActions).toContainEqual(expectedAction1)
+        expect(actionList.genericActions).toContainEqual(expectedAction2)
       })
     })
     describe('#convertJsonApiObjectListToModelList', () => {
@@ -913,7 +913,7 @@ describe('GenericActionSerializer', () => {
         expect(apiRelationship).toEqual(expectedRelationship)
       })
     })
-    describe('#convertJsonApiIncludedGenericActionAttachmentsToIdList', () => {
+    describe('#convertJsonApiIncludedActionAttachmentsToIdList', () => {
       it('should return a list of generic_device_action_attachment ids / attachment ids mappings', () => {
         const expectedMappings = [
           {
@@ -923,7 +923,7 @@ describe('GenericActionSerializer', () => {
         ]
         const serializer = new GenericDeviceActionSerializer()
         const data = getExampleObjectResponseWithIncludedActionAttachments()
-        const mappings = serializer.convertJsonApiIncludedGenericActionAttachmentsToIdList(data.included as IJsonApiEntityWithOptionalAttributes[])
+        const mappings = serializer.convertJsonApiIncludedActionAttachmentsToIdList(data.included as IJsonApiEntityWithOptionalAttributes[])
 
         expect(mappings).toEqual(expectedMappings)
       })
diff --git a/test/serializers/jsonapi/SoftwareTypeSerializer.test.ts b/test/serializers/jsonapi/SoftwareTypeSerializer.test.ts
new file mode 100644
index 000000000..88dbc2f7b
--- /dev/null
+++ b/test/serializers/jsonapi/SoftwareTypeSerializer.test.ts
@@ -0,0 +1,104 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { SoftwareType } from '@/models/SoftwareType'
+import { SoftwareTypeSerializer } from '@/serializers/jsonapi/SoftwareTypeSerializer'
+
+describe('SoftwareTypeSerializer', () => {
+  describe('#convertJsonApiObjectListToModelList', () => {
+    it('should convert a list of two elements to a model list', () => {
+      const jsonApiObjectList: any = {
+        data: [{
+          attributes: {
+            term: 'Software',
+            definition: '',
+            provenance: null,
+            provenance_uri: null,
+            category: null,
+            note: null,
+            status: 'ACCEPTED'
+          },
+          id: '1',
+          links: {
+            self: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/softwaretypes/1/'
+          },
+          relationships: {},
+          type: 'SoftwareType'
+        }, {
+          attributes: {
+            term: 'Firmware',
+            definition: '',
+            provenance: null,
+            provenance_uri: null,
+            category: null,
+            note: null,
+            status: 'ACCEPTED'
+          },
+          id: '2',
+          links: {
+            self: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/softwaretypes/2/'
+          },
+          relationships: {},
+          type: 'SoftwareType'
+        }],
+        included: [],
+        jsonapi: {
+          version: '1.0'
+        },
+        meta: {
+          count: 2
+        }
+      }
+
+      const expectedSoftwareType1 = SoftwareType.createFromObject({
+        id: '1',
+        name: 'Software',
+        definition: '',
+        uri: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/softwaretypes/1/'
+      })
+      const expectedSoftwareType2 = SoftwareType.createFromObject({
+        id: '2',
+        name: 'Firmware',
+        definition: '',
+        uri: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/softwaretypes/2/'
+      })
+
+      const serializer = new SoftwareTypeSerializer()
+
+      const softwaretypes = serializer.convertJsonApiObjectListToModelList(jsonApiObjectList)
+
+      expect(Array.isArray(softwaretypes)).toBeTruthy()
+      expect(softwaretypes.length).toEqual(2)
+      expect(softwaretypes[0]).toEqual(expectedSoftwareType1)
+      expect(softwaretypes[1]).toEqual(expectedSoftwareType2)
+    })
+  })
+})
diff --git a/test/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.test.ts b/test/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.test.ts
new file mode 100644
index 000000000..1f70e0aec
--- /dev/null
+++ b/test/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer.test.ts
@@ -0,0 +1,313 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { Attachment } from '@/models/Attachment'
+
+import {
+  DeviceSoftwareUpdateActionAttachmentSerializer,
+  PlatformSoftwareUpdateActionAttachmentSerializer
+} from '@/serializers/jsonapi/SoftwareUpdateActionAttachmentSerializer'
+
+import {
+  IJsonApiEntityEnvelope,
+  IJsonApiEntityWithOptionalId,
+  IJsonApiEntityWithOptionalAttributes,
+  IJsonApiRelationships
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+describe('SoftwareUpdateActionAttachmentSerializer', () => {
+  describe('DeviceSoftwareUpdateActionAttachmentSerializer', () => {
+    function getExampleObjectResponse (): IJsonApiEntityEnvelope {
+      return {
+        data: {
+          type: 'device_software_update_action',
+          relationships: {
+            device_software_update_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/device-software-update-actions/9/relationships/device-software-update-action-attachments'
+              },
+              data: [
+                {
+                  type: 'device_software_update_action_attachment',
+                  id: '6'
+                },
+                {
+                  type: 'device_software_update_action_attachment',
+                  id: '7'
+                }
+              ]
+            },
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/9/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/9/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/14'
+              },
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            }
+          },
+          attributes: {
+            software_type_uri: 'https://cv/firmware',
+            software_type_name: 'Firmware',
+            update_date: '2021-05-21T00:00:00',
+            description: 'dfdfdf',
+            version: '1.42',
+            repository_url: 'https://myrepo.de'
+          },
+          id: '9',
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-actions/9'
+          }
+        },
+        links: {
+          self: '/rdm/svm-api/v1/device-software-update-actions/9'
+        },
+        included: [
+          {
+            type: 'device_software_update_action_attachment',
+            relationships: {
+              attachment: {
+                links: {
+                  self: '/rdm/svm-api/v1/device-software-update-action-attachments/6/relationships/attachment',
+                  related: '/rdm/svm-api/v1/device-attachments/51'
+                },
+                data: {
+                  type: 'device_attachment',
+                  id: '51'
+                }
+              },
+              action: {
+                links: {
+                  self: '/rdm/svm-api/v1/device-software-update-action-attachments/6/relationships/action',
+                  related: '/rdm/svm-api/v1/device-software-update-actions/9'
+                }
+              }
+            },
+            id: '6',
+            links: {
+              self: '/rdm/svm-api/v1/device-software-update-action-attachments/6'
+            }
+          },
+          {
+            type: 'device_attachment',
+            relationships: {
+              device: {
+                links: {
+                  self: '/rdm/svm-api/v1/device-attachments/51/relationships/device',
+                  related: '/rdm/svm-api/v1/devices/204'
+                },
+                data: {
+                  type: 'device',
+                  id: '204'
+                }
+              }
+            },
+            attributes: {
+              url: 'https://foo.de',
+              label: 'Foo.de'
+            },
+            id: '51',
+            links: {
+              self: '/rdm/svm-api/v1/device-attachments/51'
+            }
+          },
+          {
+            type: 'device_software_update_action_attachment',
+            relationships: {
+              attachment: {
+                links: {
+                  self: '/rdm/svm-api/v1/device-software-update-action-attachments/7/relationships/attachment',
+                  related: '/rdm/svm-api/v1/device-attachments/52'
+                },
+                data: {
+                  type: 'device_attachment',
+                  id: '52'
+                }
+              },
+              action: {
+                links: {
+                  self: '/rdm/svm-api/v1/device-software-update-action-attachments/7/relationships/action',
+                  related: '/rdm/svm-api/v1/device-software-update-actions/9'
+                }
+              }
+            },
+            id: '7',
+            links: {
+              self: '/rdm/svm-api/v1/device-software-update-action-attachments/7'
+            }
+          },
+          {
+            type: 'device_attachment',
+            relationships: {
+              device: {
+                links: {
+                  self: '/rdm/svm-api/v1/device-attachments/52/relationships/device',
+                  related: '/rdm/svm-api/v1/devices/204'
+                },
+                data: {
+                  type: 'device',
+                  id: '204'
+                }
+              }
+            },
+            attributes: {
+              url: 'https://bar.baz',
+              label: 'Bar.baz'
+            },
+            id: '52',
+            links: {
+              self: '/rdm/svm-api/v1/device-attachments/52'
+            }
+          }
+        ],
+        jsonapi: {
+          version: '1.0'
+        }
+      }
+    }
+
+    describe('constructing and types', () => {
+      it('should return a correct action type name', () => {
+        const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+        expect(serializer.getActionTypeName()).toEqual('device_software_update_action')
+      })
+      it('should return a correct action attachment type name', () => {
+        const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+        expect(serializer.getActionAttachmentTypeName()).toEqual('device_software_update_action_attachment')
+      })
+      it('should return a the plural form of the action attachment type name', () => {
+        const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+        expect(serializer.getActionAttachmentTypeNamePlural()).toEqual('device_software_update_action_attachments')
+      })
+      it('should return a correct attachment type name', () => {
+        const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+        expect(serializer.getAttachmentTypeName()).toEqual('device_attachment')
+      })
+      it('should return an attachment serializer', () => {
+        const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+        expect(typeof serializer.attachmentSerializer).toBe('object')
+      })
+    })
+    describe('#convertModelToJsonApiData', () => {
+      it('should return a JSON API object from an attachment and an action id', () => {
+        const attachment = Attachment.createFromObject({
+          id: '1',
+          label: 'Foo',
+          url: 'https://bar.baz'
+        })
+        const actionId = '2'
+
+        const expectedApiModel: IJsonApiEntityWithOptionalId = {
+          type: 'device_software_update_action_attachment',
+          attributes: {},
+          relationships: {
+            action: {
+              data: {
+                type: 'device_software_update_action',
+                id: '2'
+              }
+            },
+            attachment: {
+              data: {
+                type: 'device_attachment',
+                id: '1'
+              }
+            }
+          }
+        }
+
+        const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+        const apiModel = serializer.convertModelToJsonApiData(attachment, actionId)
+
+        expect(apiModel).toEqual(expectedApiModel)
+      })
+    })
+    describe('#convertJsonApiRelationshipsModelList', () => {
+      it('should return a serialized list of attachments from an list of included API entities', () => {
+        const attachment1 = Attachment.createFromObject({
+          id: '51',
+          label: 'Foo.de',
+          url: 'https://foo.de'
+        })
+        const attachment2 = Attachment.createFromObject({
+          id: '52',
+          label: 'Bar.baz',
+          url: 'https://bar.baz'
+        })
+
+        const response = getExampleObjectResponse()
+        const serializer = new DeviceSoftwareUpdateActionAttachmentSerializer()
+
+        const attachmentList = serializer.convertJsonApiRelationshipsModelList(response.data.relationships as IJsonApiRelationships, response.included as IJsonApiEntityWithOptionalAttributes[])
+
+        expect(attachmentList).toHaveLength(2)
+        expect(attachmentList).toContainEqual(attachment1)
+        expect(attachmentList).toContainEqual(attachment2)
+      })
+    })
+  })
+  describe('PlatformSoftwareUpdateActionAttachmentSerializer', () => {
+    describe('constructing and types', () => {
+      it('should return a correct action type name', () => {
+        const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer()
+        expect(serializer.getActionTypeName()).toEqual('platform_software_update_action')
+      })
+      it('should return a correct action attachment type name', () => {
+        const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer()
+        expect(serializer.getActionAttachmentTypeName()).toEqual('platform_software_update_action_attachment')
+      })
+      it('should return a the plural form of the action attachment type name', () => {
+        const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer()
+        expect(serializer.getActionAttachmentTypeNamePlural()).toEqual('platform_software_update_action_attachments')
+      })
+      it('should return a correct attachment type name', () => {
+        const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer()
+        expect(serializer.getAttachmentTypeName()).toEqual('platform_attachment')
+      })
+      it('should return an attachment serializer', () => {
+        const serializer = new PlatformSoftwareUpdateActionAttachmentSerializer()
+        expect(typeof serializer.attachmentSerializer).toBe('object')
+      })
+    })
+  })
+})
diff --git a/test/serializers/jsonapi/SoftwareUpdateActionSerializer.test.ts b/test/serializers/jsonapi/SoftwareUpdateActionSerializer.test.ts
new file mode 100644
index 000000000..5ac37023b
--- /dev/null
+++ b/test/serializers/jsonapi/SoftwareUpdateActionSerializer.test.ts
@@ -0,0 +1,1229 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+
+import { Attachment } from '@/models/Attachment'
+import { SoftwareUpdateAction } from '@/models/SoftwareUpdateAction'
+import { Contact } from '@/models/Contact'
+
+import {
+  DeviceSoftwareUpdateActionSerializer,
+  PlatformSoftwareUpdateActionSerializer
+} from '@/serializers/jsonapi/SoftwareUpdateActionSerializer'
+
+import {
+  IJsonApiEntityEnvelope,
+  IJsonApiEntityListEnvelope,
+  IJsonApiEntityWithOptionalId,
+  IJsonApiEntityWithOptionalAttributes,
+  IJsonApiRelationships
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+describe('SoftwareUpdateActionSerializer', () => {
+  function getExampleObjectResponse (): IJsonApiEntityEnvelope {
+    return {
+      data: {
+        type: 'device_software_update_action',
+        attributes: {
+          software_type_name: 'Program',
+          version: 'fe23f4afc12f234sd',
+          software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/',
+          repository_url: 'https://foo/bar',
+          update_date: '2021-07-01T00:00:00',
+          description: 'Test',
+          created_at: '2021-06-14T14:47:53.554867',
+          updated_at: null
+        },
+        relationships: {
+          device: {
+            links: {
+              self: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/device',
+              related: '/rdm/svm-api/v1/devices/204'
+            },
+            data: {
+              type: 'device',
+              id: '204'
+            }
+          },
+          contact: {
+            links: {
+              self: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/contact',
+              related: '/rdm/svm-api/v1/contacts/14'
+            },
+            data: {
+              type: 'contact',
+              id: '14'
+            }
+          },
+          device_software_update_action_attachments: {
+            links: {
+              related: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/device-software-update-action-attachments'
+            },
+            data: [
+
+            ]
+          }
+        },
+        id: '3',
+        links: {
+          self: '/rdm/svm-api/v1/device-software-update-actions/3'
+        }
+      },
+      links: {
+        self: '/rdm/svm-api/v1/device-software-update-actions/3'
+      },
+      included: [
+        {
+          type: 'contact',
+          attributes: {
+            family_name: 'Hanisch',
+            given_name: 'Marc',
+            website: '',
+            email: 'marc.hanisch@gfz-potsdam.de'
+          },
+          relationships: {
+            devices: {
+              links: {
+                related: '/rdm/svm-api/v1/contacts/14/relationships/devices'
+              },
+              data: [
+                {
+                  type: 'device',
+                  id: '250'
+                }
+              ]
+            },
+            configurations: {
+              links: {
+                related: '/rdm/svm-api/v1/contacts/14/relationships/configurations'
+              },
+              data: [
+
+              ]
+            },
+            platforms: {
+              links: {
+                related: '/rdm/svm-api/v1/contacts/14/relationships/platforms'
+              },
+              data: [
+
+              ]
+            },
+            user: {
+              links: {
+                self: '/rdm/svm-api/v1/contacts/14/relationships/user'
+              },
+              data: {
+                type: 'user',
+                id: '6'
+              }
+            }
+          },
+          id: '14',
+          links: {
+            self: '/rdm/svm-api/v1/contacts/14'
+          }
+        }
+      ],
+      jsonapi: {
+        version: '1.0'
+      }
+    }
+  }
+
+  function getExampleObjectListResponse (): IJsonApiEntityListEnvelope {
+    return {
+      data: [
+        {
+          type: 'device_software_update_action',
+          id: '2',
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/3'
+              },
+              data: {
+                type: 'contact',
+                id: '3'
+              }
+            },
+            device_software_update_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device-software-update-action-attachments'
+              },
+              data: [
+                {
+                  type: 'device_software_update_action_attachment',
+                  id: '1'
+                },
+                {
+                  type: 'device_software_update_action_attachment',
+                  id: '2'
+                },
+                {
+                  type: 'device_software_update_action_attachment',
+                  id: '3'
+                }
+              ]
+            }
+          },
+          attributes: {
+            software_type_name: 'Firmware',
+            repository_url: 'https://foo/bar/baz',
+            description: 'Some simple description!!!',
+            update_date: '2021-06-30T00:00:00',
+            software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/',
+            version: '1.9',
+            created_at: '2021-06-14T12:18:06.531875'
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-actions/2'
+          }
+        },
+        {
+          type: 'device_software_update_action',
+          id: '1',
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/1/relationships/device',
+                related: '/rdm/svm-api/v1/devices/256'
+              },
+              data: {
+                type: 'device',
+                id: '256'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/1/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/14'
+              },
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            },
+            device_software_update_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/device-software-update-actions/1/relationships/device-software-update-action-attachments'
+              },
+              data: [
+
+              ]
+            }
+          },
+          attributes: {
+            software_type_name: 'Firmware',
+            repository_url: 'https://foo.bar',
+            description: 'Some description',
+            update_date: '2021-06-03T00:00:00',
+            software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/',
+            version: '1.3'
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-actions/1'
+          }
+        }
+      ],
+      links: {
+        self: 'http://rz-vm64.gfz-potsdam.de:5000/rdm/svm-api/v1/device-software-update-actions?include=contact%2Cdevice_software_update_action_attachments.attachment'
+      },
+      included: [
+        {
+          type: 'contact',
+          id: '3',
+          attributes: {
+            family_name: 'Brinckmann',
+            given_name: 'Nils',
+            email: 'nils.brinckmann@gfz-potsdam.de',
+            website: 'https://www.gfz-potsdam.de/staff/nils-brinckmann/'
+          },
+          relationships: {
+            user: {
+              links: {
+                self: '/rdm/svm-api/v1/contacts/3/relationships/user'
+              },
+              data: {
+                type: 'user',
+                id: '3'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/contacts/3'
+          }
+        },
+        {
+          type: 'device_software_update_action_attachment',
+          relationships: {
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/1/relationships/action',
+                related: '/rdm/svm-api/v1/device-software-update-action-attachments/2'
+              },
+              data: {
+                type: 'device_software_update_action',
+                id: '2'
+              }
+            },
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/1/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/53'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '53'
+              }
+            }
+          },
+          id: '1',
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-action-attachments/1'
+          }
+        },
+        {
+          type: 'device_attachment',
+          id: '53',
+          attributes: {
+            label: 'GFZ',
+            url: 'https://www.gfz-potsdam.de'
+          },
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/53/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/53'
+          }
+        },
+        {
+          type: 'device_software_update_action_attachment',
+          relationships: {
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/2/relationships/action',
+                related: '/rdm/svm-api/v1/device-software-update-action-attachments/2'
+              },
+              data: {
+                type: 'device_software_update_action',
+                id: '2'
+              }
+            },
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/2/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/52'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '52'
+              }
+            }
+          },
+          id: '2',
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-action-attachments/2'
+          }
+        },
+        {
+          type: 'device_attachment',
+          id: '52',
+          attributes: {
+            label: 'Bar.baz',
+            url: 'https://bar.baz'
+          },
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/52/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/52'
+          }
+        },
+        {
+          type: 'device_software_update_action_attachment',
+          relationships: {
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/3/relationships/action',
+                related: '/rdm/svm-api/v1/device-software-update-action-attachments/2'
+              },
+              data: {
+                type: 'device_software_update_action',
+                id: '2'
+              }
+            },
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/3/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/51'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '51'
+              }
+            }
+          },
+          id: '3',
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-action-attachments/3'
+          }
+        },
+        {
+          type: 'device_attachment',
+          id: '51',
+          attributes: {
+            label: 'Foo.de',
+            url: 'https://foo.de'
+          },
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/51/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/51'
+          }
+        },
+        {
+          type: 'contact',
+          id: '14',
+          attributes: {
+            family_name: 'Hanisch',
+            given_name: 'Marc',
+            email: 'marc.hanisch@gfz-potsdam.de',
+            website: ''
+          },
+          relationships: {
+            user: {
+              links: {
+                self: '/rdm/svm-api/v1/contacts/14/relationships/user'
+              },
+              data: {
+                type: 'user',
+                id: '6'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/contacts/14'
+          }
+        }
+      ],
+      meta: {
+        count: 2
+      },
+      jsonapi: {
+        version: '1.0'
+      }
+    }
+  }
+
+  function getExampleDeviceResponse (): IJsonApiEntityEnvelope {
+    return {
+      data: {
+        type: 'device',
+        attributes: {
+          persistent_identifier: null,
+          manufacturer_name: 'OTT Hydromet GmbH',
+          model: 'SM1',
+          updated_at: '2021-04-26T09:03:01.944689',
+          device_type_name: 'Frequency/Time Domain Reflectometer (FTDR)(Soil moisture and temperature)',
+          long_name: 'Adcon SM 1 soil moisture / temperature sensor',
+          short_name: 'Adcon SM1 soil moisture / temperature sensor FTDR Zeitlow 1',
+          website: 'http://www.adcon.com',
+          status_name: 'In Use',
+          manufacturer_uri: 'OTT Hydromet GmbH',
+          created_at: '2021-01-18T07:07:24.360000',
+          serial_number: '',
+          device_type_uri: '',
+          dual_use: false,
+          description: '',
+          status_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/equipmentstatus/2/',
+          inventory_number: ''
+        },
+        relationships: {
+          device_properties: {
+            links: {
+              related: '/rdm/svm-api/v1/devices/204/relationships/device-properties'
+            },
+            data: [
+              {
+                type: 'device_property',
+                id: '150'
+              }
+            ]
+          },
+          contacts: {
+            links: {
+              related: '/rdm/svm-api/v1/devices/204/relationships/contacts'
+            },
+            data: [
+              {
+                type: 'contact',
+                id: '10'
+              },
+              {
+                type: 'contact',
+                id: '21'
+              }
+            ]
+          },
+          device_software_update_actions: {
+            links: {
+              related: '/rdm/svm-api/v1/devices/204/relationships/device-software-update-actions'
+            },
+            data: [
+              {
+                type: 'device_software_update_action',
+                id: '2'
+              },
+              {
+                type: 'device_software_update_action',
+                id: '3'
+              }
+            ]
+          },
+          device_attachments: {
+            links: {
+              related: '/rdm/svm-api/v1/devices/204/relationships/device-attachments'
+            },
+            data: [
+              {
+                type: 'device_attachment',
+                id: '51'
+              },
+              {
+                type: 'device_attachment',
+                id: '52'
+              },
+              {
+                type: 'device_attachment',
+                id: '53'
+              }
+            ]
+          }
+        },
+        id: '204',
+        links: {
+          self: '/rdm/svm-api/v1/devices/204'
+        }
+      },
+      links: {
+        self: '/rdm/svm-api/v1/devices/204'
+      },
+      included: [
+        {
+          type: 'device_software_update_action',
+          id: '2',
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/3'
+              },
+              data: {
+                type: 'contact',
+                id: '3'
+              }
+            },
+            device_software_update_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device-software-update-action-attachments'
+              },
+              data: [
+                {
+                  type: 'device_software_update_action_attachment',
+                  id: '1'
+                },
+                {
+                  type: 'device_software_update_action_attachment',
+                  id: '2'
+                },
+                {
+                  type: 'device_software_update_action_attachment',
+                  id: '3'
+                }
+              ]
+            }
+          },
+          attributes: {
+            software_type_name: 'Firmware',
+            repository_url: 'https://foo/bar/baz',
+            description: 'Some simple description!!!',
+            updated_at: '2021-06-14T14:37:42.105091',
+            update_date: '2021-06-30T00:00:00',
+            software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/',
+            version: '1.9',
+            created_at: '2021-06-14T12:18:06.531875'
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-actions/2'
+          }
+        },
+        {
+          type: 'device_software_update_action',
+          id: '3',
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/14'
+              },
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            },
+            device_software_update_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/device-software-update-actions/3/relationships/device-software-update-action-attachments'
+              },
+              data: [
+
+              ]
+            }
+          },
+          attributes: {
+            software_type_name: 'Program',
+            repository_url: '',
+            description: 'Test',
+            updated_at: null,
+            update_date: '2021-07-01T00:00:00',
+            software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/',
+            version: 'fe23f4afc12f234sd',
+            created_at: '2021-06-14T14:47:53.554867'
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-actions/3'
+          }
+        }
+      ],
+      jsonapi: {
+        version: '1.0'
+      }
+    }
+  }
+
+  function getExampleObjectResponseWithIncludedActionAttachments (): IJsonApiEntityEnvelope {
+    return {
+      data: {
+        type: 'device_software_update_action',
+        id: '2',
+        relationships: {
+          device: {
+            links: {
+              self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device',
+              related: '/rdm/svm-api/v1/devices/204'
+            },
+            data: {
+              type: 'device',
+              id: '204'
+            }
+          },
+          contact: {
+            links: {
+              self: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/contact',
+              related: '/rdm/svm-api/v1/contacts/3'
+            },
+            data: {
+              type: 'contact',
+              id: '3'
+            }
+          },
+          device_software_update_action_attachments: {
+            links: {
+              related: '/rdm/svm-api/v1/device-software-update-actions/2/relationships/device-software-update-action-attachments'
+            },
+            data: [
+              {
+                type: 'device_software_update_action_attachment',
+                id: '1'
+              },
+              {
+                type: 'device_software_update_action_attachment',
+                id: '2'
+              },
+              {
+                type: 'device_software_update_action_attachment',
+                id: '3'
+              }
+            ]
+          }
+        },
+        attributes: {
+          software_type_name: 'Firmware',
+          repository_url: 'https://foo/bar/baz',
+          description: 'Some simple description!!!',
+          updated_at: '2021-06-14T14:37:42.105091',
+          update_date: '2021-06-30T00:00:00',
+          software_type_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/',
+          version: '1.9',
+          created_at: '2021-06-14T12:18:06.531875'
+        },
+        links: {
+          self: '/rdm/svm-api/v1/device-software-update-actions/2'
+        }
+      },
+      links: {
+        self: '/rdm/svm-api/v1/device-software-update-actions/2'
+      },
+      included: [
+        {
+          type: 'contact',
+          id: '3',
+          attributes: {
+            family_name: 'Brinckmann',
+            given_name: 'Nils',
+            email: 'nils.brinckmann@gfz-potsdam.de',
+            website: 'https://www.gfz-potsdam.de/staff/nils-brinckmann/'
+          },
+          relationships: {
+            user: {
+              links: {
+                self: '/rdm/svm-api/v1/contacts/3/relationships/user'
+              },
+              data: {
+                type: 'user',
+                id: '3'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/contacts/3'
+          }
+        },
+        {
+          type: 'device_software_update_action_attachment',
+          relationships: {
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/1/relationships/action',
+                related: '/rdm/svm-api/v1/device-software-update-action-attachments/2'
+              },
+              data: {
+                type: 'device_software_update_action',
+                id: '2'
+              }
+            },
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/1/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/53'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '53'
+              }
+            }
+          },
+          id: '1',
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-action-attachments/1'
+          }
+        },
+        {
+          type: 'device_attachment',
+          id: '53',
+          attributes: {
+            label: 'GFZ',
+            url: 'https://www.gfz-potsdam.de'
+          },
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/53/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/53'
+          }
+        },
+        {
+          type: 'device_software_update_action_attachment',
+          relationships: {
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/2/relationships/action',
+                related: '/rdm/svm-api/v1/device-software-update-action-attachments/2'
+              },
+              data: {
+                type: 'device_software_update_action',
+                id: '2'
+              }
+            },
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/2/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/52'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '52'
+              }
+            }
+          },
+          id: '2',
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-action-attachments/2'
+          }
+        },
+        {
+          type: 'device_attachment',
+          id: '52',
+          attributes: {
+            label: 'Bar.baz',
+            url: 'https://bar.baz'
+          },
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/52/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/52'
+          }
+        },
+        {
+          type: 'device_software_update_action_attachment',
+          relationships: {
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/3/relationships/action',
+                related: '/rdm/svm-api/v1/device-software-update-action-attachments/2'
+              },
+              data: {
+                type: 'device_software_update_action',
+                id: '2'
+              }
+            },
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/device-software-update-action-attachments/3/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/51'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '51'
+              }
+            }
+          },
+          id: '3',
+          links: {
+            self: '/rdm/svm-api/v1/device-software-update-action-attachments/3'
+          }
+        },
+        {
+          type: 'device_attachment',
+          id: '51',
+          attributes: {
+            label: 'Foo.de',
+            url: 'https://foo.de'
+          },
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/51/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/51'
+          }
+        }
+      ],
+      jsonapi: {
+        version: '1.0'
+      }
+    }
+  }
+
+  describe('DeviceSoftwareUpdateActionSerializer', () => {
+    describe('constructing and types', () => {
+      it('should return \'device\' as its type', () => {
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        expect(serializer.targetType).toEqual('device')
+      })
+      it('should return a correct action type name', () => {
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        expect(serializer.getActionTypeName()).toEqual('device_software_update_action')
+      })
+      it('should return a the plural form of the action type name', () => {
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        expect(serializer.getActionTypeNamePlural()).toEqual('device_software_update_actions')
+      })
+      it('should return a correction action attachment type name', () => {
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        expect(serializer.getActionAttachmentTypeName()).toEqual('device_software_update_action_attachment')
+      })
+    })
+    describe('#convertJsonApiObjectToModel', () => {
+      it('should return a serialized software update action from an API response', () => {
+        const contact = Contact.createFromObject({
+          id: '14',
+          givenName: 'Marc',
+          familyName: 'Hanisch',
+          email: 'marc.hanisch@gfz-potsdam.de',
+          website: ''
+        })
+        const expectedAction = new SoftwareUpdateAction()
+        expectedAction.id = '3'
+        expectedAction.description = 'Test'
+        expectedAction.softwareTypeName = 'Program'
+        expectedAction.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/'
+        expectedAction.updateDate = DateTime.fromISO('2021-07-01T00:00:00', { zone: 'UTC' })
+        expectedAction.contact = contact
+        expectedAction.version = 'fe23f4afc12f234sd'
+        expectedAction.repositoryUrl = 'https://foo/bar'
+
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        const action = serializer.convertJsonApiObjectToModel(getExampleObjectResponse())
+
+        expect(action).toEqual(expectedAction)
+      })
+    })
+    describe('#convertJsonApiDataToModel', () => {
+      it('should return a serialized software update action from an API response object', () => {
+        const contact = Contact.createFromObject({
+          id: '14',
+          givenName: 'Marc',
+          familyName: 'Hanisch',
+          email: 'marc.hanisch@gfz-potsdam.de',
+          website: ''
+        })
+        const expectedAction = new SoftwareUpdateAction()
+        expectedAction.id = '3'
+        expectedAction.description = 'Test'
+        expectedAction.softwareTypeName = 'Program'
+        expectedAction.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/'
+        expectedAction.updateDate = DateTime.fromISO('2021-07-01T00:00:00', { zone: 'UTC' })
+        expectedAction.contact = contact
+        expectedAction.version = 'fe23f4afc12f234sd'
+        expectedAction.repositoryUrl = 'https://foo/bar'
+
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        const data = getExampleObjectResponse().data
+        const included = getExampleObjectResponse().included
+        const action = serializer.convertJsonApiDataToModel(data, included as IJsonApiEntityWithOptionalAttributes[])
+
+        expect(action).toEqual(expectedAction)
+      })
+    })
+    describe('#convertJsonApiRelationshipsModelList', () => {
+      it('should return a serialized list of software update actions from an list of included API entities', () => {
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        const response = getExampleDeviceResponse()
+
+        const relationships = response.data.relationships
+        const included = response.included
+
+        const expectedAction1 = new SoftwareUpdateAction()
+        expectedAction1.id = '2'
+        expectedAction1.description = 'Some simple description!!!'
+        expectedAction1.softwareTypeName = 'Firmware'
+        expectedAction1.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/'
+        expectedAction1.updateDate = DateTime.fromISO('2021-06-30T00:00:00', { zone: 'UTC' })
+        expectedAction1.version = '1.9'
+        expectedAction1.repositoryUrl = 'https://foo/bar/baz'
+
+        const expectedAction2 = new SoftwareUpdateAction()
+        expectedAction2.id = '3'
+        expectedAction2.description = 'Test'
+        expectedAction2.softwareTypeName = 'Program'
+        expectedAction2.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/2/'
+        expectedAction2.updateDate = DateTime.fromISO('2021-07-01T00:00:00', { zone: 'UTC' })
+        expectedAction2.version = 'fe23f4afc12f234sd'
+        expectedAction2.repositoryUrl = ''
+
+        const actionList = serializer.convertJsonApiRelationshipsModelList(relationships as IJsonApiRelationships, included as IJsonApiEntityWithOptionalAttributes[])
+
+        expect(actionList).toHaveProperty('softwareUpdateActions')
+        expect(actionList.softwareUpdateActions).toContainEqual(expectedAction1)
+        expect(actionList.softwareUpdateActions).toContainEqual(expectedAction2)
+      })
+    })
+    describe('#convertJsonApiObjectListToModelList', () => {
+      it('should return a list of serialized software update actions from an API response', () => {
+        const contact1 = Contact.createFromObject({
+          id: '14',
+          givenName: 'Marc',
+          familyName: 'Hanisch',
+          email: 'marc.hanisch@gfz-potsdam.de',
+          website: ''
+        })
+        const contact2 = Contact.createFromObject({
+          id: '3',
+          givenName: 'Nils',
+          familyName: 'Brinckmann',
+          email: 'nils.brinckmann@gfz-potsdam.de',
+          website: 'https://www.gfz-potsdam.de/staff/nils-brinckmann/'
+        })
+        const attachment1 = Attachment.createFromObject({
+          id: '51',
+          label: 'Foo.de',
+          url: 'https://foo.de'
+        })
+        const attachment2 = Attachment.createFromObject({
+          id: '52',
+          label: 'Bar.baz',
+          url: 'https://bar.baz'
+        })
+        const attachment3 = Attachment.createFromObject({
+          id: '53',
+          label: 'GFZ',
+          url: 'https://www.gfz-potsdam.de'
+        })
+
+        const expectedAction1 = new SoftwareUpdateAction()
+        expectedAction1.id = '2'
+        expectedAction1.description = 'Some simple description!!!'
+        expectedAction1.softwareTypeName = 'Firmware'
+        expectedAction1.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/'
+        expectedAction1.updateDate = DateTime.fromISO('2021-06-30T00:00:00', { zone: 'UTC' })
+        expectedAction1.version = '1.9'
+        expectedAction1.repositoryUrl = 'https://foo/bar/baz'
+        expectedAction1.contact = contact2
+        expectedAction1.attachments = [
+          attachment3,
+          attachment2,
+          attachment1
+        ]
+
+        const expectedAction2 = new SoftwareUpdateAction()
+        expectedAction2.id = '1'
+        expectedAction2.description = 'Some description'
+        expectedAction2.softwareTypeName = 'Firmware'
+        expectedAction2.softwareTypeUrl = 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/softwaretypes/1/'
+        expectedAction2.updateDate = DateTime.fromISO('2021-06-03T00:00:00', { zone: 'UTC' })
+        expectedAction2.version = '1.3'
+        expectedAction2.repositoryUrl = 'https://foo.bar'
+        expectedAction2.contact = contact1
+
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        const actionList = serializer.convertJsonApiObjectListToModelList(getExampleObjectListResponse())
+
+        expect(actionList).toContainEqual(expectedAction1)
+        expect(actionList).toContainEqual(expectedAction2)
+      })
+    })
+    describe('#convertModelToJsonApiData', () => {
+      it('should return a JSON API representation from a software update action model', () => {
+        const contact = Contact.createFromObject({
+          id: '14',
+          givenName: 'Marc',
+          familyName: 'Hanisch',
+          email: 'marc.hanisch@gfz-potsdam.de',
+          website: ''
+        })
+
+        const action = new SoftwareUpdateAction()
+        action.id = '7'
+        action.description = 'Bla'
+        action.softwareTypeName = 'Firmware'
+        action.softwareTypeUrl = 'https://foo/bar'
+        action.updateDate = DateTime.fromISO('2021-05-23T00:00:00', { zone: 'UTC' })
+        action.version = '10.2'
+        action.repositoryUrl = 'https://git.gfz-potsdam.de/sms/frontend'
+        action.contact = contact
+
+        const expectedApiModel: IJsonApiEntityWithOptionalId = {
+          type: 'device_software_update_action',
+          id: '7',
+          attributes: {
+            description: 'Bla',
+            software_type_name: 'Firmware',
+            software_type_uri: 'https://foo/bar',
+            update_date: '2021-05-23T00:00:00.000Z',
+            version: '10.2',
+            repository_url: 'https://git.gfz-potsdam.de/sms/frontend'
+          },
+          relationships: {
+            device: {
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            }
+          }
+        }
+
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        const apiModel = serializer.convertModelToJsonApiData(action, '204')
+
+        expect(apiModel).toEqual(expectedApiModel)
+      })
+    })
+    describe('#convertModelToJsonApiRelationshipObject', () => {
+      it('should return a JSON API relationships object from a software update action model', () => {
+        const action = new SoftwareUpdateAction()
+        action.id = '7'
+
+        const expectedRelationship: IJsonApiRelationships = {
+          device_software_update_action: {
+            data: {
+              id: '7',
+              type: 'device_software_update_action'
+            }
+          }
+        }
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        const apiRelationship = serializer.convertModelToJsonApiRelationshipObject(action)
+
+        expect(apiRelationship).toEqual(expectedRelationship)
+      })
+    })
+    describe('#convertJsonApiIncludedActionAttachmentsToIdList', () => {
+      it('should return a list of device_software_update_action_attachment ids / attachment ids mappings', () => {
+        const expectedMappings = [
+          {
+            softwareUpdateActionAttachmentId: '1',
+            attachmentId: '53'
+          },
+          {
+            softwareUpdateActionAttachmentId: '2',
+            attachmentId: '52'
+          },
+          {
+            softwareUpdateActionAttachmentId: '3',
+            attachmentId: '51'
+          }
+        ]
+        const serializer = new DeviceSoftwareUpdateActionSerializer()
+        const data = getExampleObjectResponseWithIncludedActionAttachments()
+        const mappings = serializer.convertJsonApiIncludedActionAttachmentsToIdList(data.included as IJsonApiEntityWithOptionalAttributes[])
+
+        expect(mappings).toEqual(expectedMappings)
+      })
+    })
+  })
+
+  describe('PlatformSoftwareUpdateActionSerializer', () => {
+    describe('constructing and types', () => {
+      it('should return \'platform\' as its type', () => {
+        const serializer = new PlatformSoftwareUpdateActionSerializer()
+        expect(serializer.targetType).toEqual('platform')
+      })
+      it('should return a correct action type name', () => {
+        const serializer = new PlatformSoftwareUpdateActionSerializer()
+        expect(serializer.getActionTypeName()).toEqual('platform_software_update_action')
+      })
+      it('should return a the plural form of the action type name', () => {
+        const serializer = new PlatformSoftwareUpdateActionSerializer()
+        expect(serializer.getActionTypeNamePlural()).toEqual('platform_software_update_actions')
+      })
+      it('should return a correction action attachment type name', () => {
+        const serializer = new PlatformSoftwareUpdateActionSerializer()
+        expect(serializer.getActionAttachmentTypeName()).toEqual('platform_software_update_action_attachment')
+      })
+    })
+  })
+})
diff --git a/test/utils/urlHelper.test.ts b/test/utils/urlHelper.test.ts
index 1cd86c366..d793d5f1d 100644
--- a/test/utils/urlHelper.test.ts
+++ b/test/utils/urlHelper.test.ts
@@ -29,7 +29,7 @@
  * implied. See the Licence for the specific language governing
  * permissions and limitations under the Licence.
  */
-import { removeBaseUrl, removeFirstSlash, removeTrailingSlash, toRouterPath } from '@/utils/urlHelpers'
+import { protocolsInUrl, removeBaseUrl, removeFirstSlash, removeTrailingSlash, toRouterPath } from '@/utils/urlHelpers'
 
 describe('removeBaseUrl', () => {
   it('should return the url if the base url is undefined', () => {
@@ -109,3 +109,17 @@ describe('toRouterPath', () => {
     expect(result).toEqual('/logout-callback')
   })
 })
+describe('protocolsInUrl', () => {
+  it('should return true with the correct protocols', () => {
+    const allowedProtocols = ['http', 'https']
+    const url = 'http://www.heise.de'
+    const urlSecure = 'https://www.heise.de'
+    expect(protocolsInUrl(allowedProtocols, url)).toBeTruthy()
+    expect(protocolsInUrl(allowedProtocols, urlSecure)).toBeTruthy()
+  })
+  it('should return false with a protocol that is not supported', () => {
+    const allowedProtocols = ['http', 'https']
+    const url = 'ftp://www.heise.de'
+    expect(protocolsInUrl(allowedProtocols, url)).toBeFalsy()
+  })
+})
diff --git a/utils/urlHelpers.ts b/utils/urlHelpers.ts
index ba937c2db..e97435b1a 100644
--- a/utils/urlHelpers.ts
+++ b/utils/urlHelpers.ts
@@ -68,3 +68,21 @@ export function toRouterPath (callbackUri: string, routeBase = '/') {
   }
   return null
 }
+
+/**
+ * checks whether the url contains the given protocols
+ *
+ * to be honest, it just checks whether the string starts with http(s):// or similar
+ *
+ * @param {string[]} allowedProtocols - the protocols to check
+ * @param {string} url - the url to check
+ * @returns {boolean | string} true when protocols are in the url, otherwise false
+ */
+export function protocolsInUrl (allowedProtocols: string[], url: string) {
+  const protocols = allowedProtocols.join('|')
+  const urlRegExp = new RegExp('^(' + protocols + ')://.+$', 'i')
+  if (url && !url.match(urlRegExp)) {
+    return false
+  }
+  return true
+}
-- 
GitLab