From 493d256db43bf1fc6cbdc4c17513d0966e3b38ea Mon Sep 17 00:00:00 2001
From: Jonathan Adeline <jonathan@baserow.io>
Date: Fri, 26 Jan 2024 08:56:35 +0000
Subject: [PATCH] SwitchInput refactoring

---
 .../components/admin/AuthProviderItem.vue     |   2 +-
 .../member-roles/MemberRolesShareToggle.vue   |   2 +-
 .../views/BaserowLogoShareLinkOption.vue      |   1 +
 .../core/assets/scss/components/all.scss      |   1 +
 .../core/assets/scss/components/form.scss     | 127 ------------------
 .../core/assets/scss/components/hidings.scss  |   2 +-
 .../assets/scss/components/switch_input.scss  | 127 ++++++++++++++++++
 .../modules/core/components/SwitchInput.vue   |  33 +++--
 .../modules/core/pages/admin/settings.vue     |   5 -
 .../modules/core/pages/styleGuide.vue         |  42 ++++--
 .../database/components/settings/APIToken.vue |   4 +-
 .../components/settings/APITokenSettings.vue  |   6 +-
 .../components/view/ShareViewLink.vue         |   2 +
 .../components/view/ViewFieldsContext.vue     |   3 +-
 .../components/view/ViewFilterForm.vue        |   1 +
 .../components/view/ViewSearchContext.vue     |   3 +-
 .../components/view/form/FormViewField.vue    |   3 +
 .../view/form/FormViewMetaControls.vue        |   2 +-
 web-frontend/stories/SwitchInput.stories.mdx  |  99 ++++++++++++++
 .../unit/core/components/switchInput.spec.js  |  70 ++++++++++
 .../__snapshots__/viewFilterForm.spec.js.snap |  18 +--
 21 files changed, 378 insertions(+), 175 deletions(-)
 create mode 100644 web-frontend/modules/core/assets/scss/components/switch_input.scss
 create mode 100644 web-frontend/stories/SwitchInput.stories.mdx
 create mode 100644 web-frontend/test/unit/core/components/switchInput.spec.js

diff --git a/enterprise/web-frontend/modules/baserow_enterprise/components/admin/AuthProviderItem.vue b/enterprise/web-frontend/modules/baserow_enterprise/components/admin/AuthProviderItem.vue
index 8ad4907d4..de6abda24 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/components/admin/AuthProviderItem.vue
+++ b/enterprise/web-frontend/modules/baserow_enterprise/components/admin/AuthProviderItem.vue
@@ -36,8 +36,8 @@
     </div>
     <SwitchInput
       class="auth-provider-admin__item-toggle"
+      small
       :value="authProvider.enabled"
-      :large="true"
       :disabled="isOneProviderEnabled && authProvider.enabled"
       @input="setEnabled($event)"
     ></SwitchInput>
diff --git a/enterprise/web-frontend/modules/baserow_enterprise/components/member-roles/MemberRolesShareToggle.vue b/enterprise/web-frontend/modules/baserow_enterprise/components/member-roles/MemberRolesShareToggle.vue
index 21de9abea..6bd1066ce 100644
--- a/enterprise/web-frontend/modules/baserow_enterprise/components/member-roles/MemberRolesShareToggle.vue
+++ b/enterprise/web-frontend/modules/baserow_enterprise/components/member-roles/MemberRolesShareToggle.vue
@@ -8,8 +8,8 @@
     </div>
     <SwitchInput
       disabled
+      small
       :value="toggled"
-      large
       @input="$emit('update:toggled', $event)"
     ></SwitchInput>
   </div>
diff --git a/premium/web-frontend/modules/baserow_premium/components/views/BaserowLogoShareLinkOption.vue b/premium/web-frontend/modules/baserow_premium/components/views/BaserowLogoShareLinkOption.vue
index 19baef3ea..a3c8b69d6 100644
--- a/premium/web-frontend/modules/baserow_premium/components/views/BaserowLogoShareLinkOption.vue
+++ b/premium/web-frontend/modules/baserow_premium/components/views/BaserowLogoShareLinkOption.vue
@@ -6,6 +6,7 @@
     @click="click"
   >
     <SwitchInput
+      small
       :value="!view.show_logo"
       :disabled="!hasPremiumFeatures"
       @input="update"
diff --git a/web-frontend/modules/core/assets/scss/components/all.scss b/web-frontend/modules/core/assets/scss/components/all.scss
index ad8b233ad..613cf9599 100644
--- a/web-frontend/modules/core/assets/scss/components/all.scss
+++ b/web-frontend/modules/core/assets/scss/components/all.scss
@@ -4,6 +4,7 @@
 @import 'button';
 @import 'alert';
 @import 'form';
+@import 'switch_input';
 @import 'box';
 @import 'layout';
 @import 'tree';
diff --git a/web-frontend/modules/core/assets/scss/components/form.scss b/web-frontend/modules/core/assets/scss/components/form.scss
index 7232389c4..011d63ac2 100644
--- a/web-frontend/modules/core/assets/scss/components/form.scss
+++ b/web-frontend/modules/core/assets/scss/components/form.scss
@@ -374,133 +374,6 @@
   animation: radio__loading 0.5s linear infinite;
 }
 
-.switch {
-  position: relative;
-  cursor: pointer;
-  user-select: none;
-  height: 28px;
-  line-height: 28px;
-  @include flex-align-items(10px);
-
-  &.switch--static {
-    cursor: initial;
-    user-select: initial;
-  }
-
-  &::before {
-    content: '';
-    @include rounded($rounded-xl);
-
-    width: 24px;
-    height: 16px;
-    background-color: $color-neutral-600;
-  }
-
-  &::after {
-    content: '';
-    border-radius: 100%;
-    width: 12px;
-    height: 12px;
-    background-color: $white;
-    transition: left 0.1s ease-in;
-    box-shadow: 0 1px 3px 0 rgba(7, 13, 16, 0.15),
-      0 0 1px 0 rgba(7, 13, 16, 0.15);
-
-    @include absolute(8px, auto, auto, 2px);
-  }
-
-  &.unknown {
-    &::before {
-      background-color: $palette-yellow-500;
-    }
-
-    &:hover {
-      &::before {
-        background-color: $palette-yellow-800;
-      }
-    }
-
-    &::after {
-      left: 6px;
-    }
-  }
-
-  &:hover {
-    &::before {
-      background-color: $color-neutral-500;
-    }
-  }
-
-  &.active {
-    &:not(.switch--disabled) {
-      &::before {
-        background-color: $palette-green-500;
-      }
-
-      &:hover {
-        &::before {
-          background-color: $palette-green-800;
-        }
-      }
-    }
-
-    &::after {
-      left: 10px;
-    }
-  }
-
-  &.switch--disabled {
-    cursor: not-allowed;
-
-    &::before {
-      background-color: $palette-neutral-400;
-    }
-
-    &::after {
-      box-shadow: none;
-    }
-  }
-
-  &.switch--large {
-    &::before {
-      height: 24px;
-      width: 40px;
-    }
-
-    &::after {
-      top: 4px;
-      width: 20px;
-      height: 20px;
-    }
-
-    &.unknown {
-      &::after {
-        left: 8px;
-      }
-    }
-
-    &.active::after {
-      left: 17px;
-    }
-  }
-}
-
-.switch__label {
-  display: flex;
-  align-items: center;
-  gap: 5px;
-  color: $palette-neutral-900;
-  cursor: pointer;
-
-  .switch--disabled & {
-    color: $palette-neutral-700;
-  }
-
-  .switch.active:not(.switch--disabled) & {
-    color: $palette-neutral-1200;
-  }
-}
-
 .error {
   margin-top: 14px;
   color: $palette-red-800;
diff --git a/web-frontend/modules/core/assets/scss/components/hidings.scss b/web-frontend/modules/core/assets/scss/components/hidings.scss
index dbb458af9..a3b058f6b 100644
--- a/web-frontend/modules/core/assets/scss/components/hidings.scss
+++ b/web-frontend/modules/core/assets/scss/components/hidings.scss
@@ -52,7 +52,7 @@
 
   position: relative;
   display: flex;
-  margin-bottom: 2px;
+  margin-bottom: 10px;
   padding: 0;
 }
 
diff --git a/web-frontend/modules/core/assets/scss/components/switch_input.scss b/web-frontend/modules/core/assets/scss/components/switch_input.scss
new file mode 100644
index 000000000..d458265f3
--- /dev/null
+++ b/web-frontend/modules/core/assets/scss/components/switch_input.scss
@@ -0,0 +1,127 @@
+.switch {
+  position: relative;
+  cursor: pointer;
+  user-select: none;
+  display: flex;
+  align-items: center;
+
+  &.switch--static {
+    cursor: initial;
+    user-select: initial;
+  }
+
+  &::before {
+    content: '';
+    width: 40px;
+    height: 24px;
+    background-color: $palette-neutral-400;
+
+    @include rounded($rounded-xl);
+  }
+
+  &:hover {
+    &::before {
+      background-color: $palette-neutral-500;
+    }
+  }
+
+  &::after {
+    content: '';
+    border-radius: 100%;
+    width: 20px;
+    height: 20px;
+    background-color: $white;
+    transition: left 0.1s ease-in;
+
+    @include add-elevation($elevation-medium);
+    @include absolute(2px, auto, auto, 2px);
+  }
+
+  &.switch--active {
+    &::before {
+      background-color: $palette-green-500;
+    }
+
+    &:not(.switch--disabled):hover {
+      &::before {
+        background-color: $palette-green-800;
+      }
+    }
+
+    &::after {
+      left: 17px;
+    }
+  }
+
+  &.switch--indeterminate {
+    &::before {
+      background-color: $palette-yellow-500;
+    }
+
+    &:not(.switch--disabled):hover {
+      &::before {
+        background-color: $palette-yellow-800;
+      }
+    }
+
+    &::after {
+      left: 10px;
+    }
+  }
+
+  &.switch--disabled {
+    cursor: not-allowed;
+
+    &::before {
+      background-color: $palette-neutral-400;
+    }
+
+    &::after {
+      box-shadow: none;
+    }
+  }
+
+  &.switch--small {
+    line-height: 16px;
+
+    &::before {
+      height: 16px;
+      width: 24px;
+    }
+
+    &::after {
+      top: 2px;
+      width: 12px;
+      height: 12px;
+    }
+
+    &.switch--active::after {
+      left: 10px;
+    }
+
+    &.switch--indeterminate::after {
+      left: 6px;
+    }
+  }
+}
+
+.switch__label {
+  margin-left: 8px;
+  display: flex;
+  align-items: center;
+  gap: 5px;
+  font-weight: 500;
+  color: $palette-neutral-900;
+
+  i {
+    font-size: 16px;
+  }
+
+  .switch--active & {
+    color: $palette-neutral-1200;
+  }
+
+  .switch--disabled & {
+    color: $palette-neutral-700;
+  }
+}
diff --git a/web-frontend/modules/core/components/SwitchInput.vue b/web-frontend/modules/core/components/SwitchInput.vue
index e9138d87a..4c24f9e5e 100644
--- a/web-frontend/modules/core/components/SwitchInput.vue
+++ b/web-frontend/modules/core/components/SwitchInput.vue
@@ -1,6 +1,6 @@
 <template>
   <div class="switch" :class="classNames" @click="toggle(value)">
-    <label v-if="hasSlot" class="switch__label"> <slot></slot></label>
+    <div v-if="hasSlot" class="switch__label"><slot></slot></div>
   </div>
 </template>
 
@@ -8,16 +8,25 @@
 export default {
   name: 'SwitchInput',
   props: {
+    /**
+     * The value of the switch.
+     */
     value: {
       type: [Boolean, Number],
       required: false,
       default: false,
     },
-    large: {
+    /**
+     * The size of the switch.
+     */
+    small: {
       type: Boolean,
       required: false,
       default: false,
     },
+    /**
+     * Whether the switch is disabled.
+     */
     disabled: {
       type: Boolean,
       required: false,
@@ -25,21 +34,17 @@ export default {
     },
   },
   computed: {
-    classNames() {
-      return {
-        'switch--has-content': Object.prototype.hasOwnProperty.call(
-          this.$slots,
-          'default'
-        ),
-        'switch--large': this.large,
-        'switch--disabled': this.disabled,
-        active: this.value === true,
-        unknown: this.value !== true && this.value !== false,
-      }
-    },
     hasSlot() {
       return !!this.$slots.default
     },
+    classNames() {
+      return {
+        'switch--small': this.small,
+        'switch--disabled': this.disabled,
+        'switch--active': this.value,
+        'switch--indeterminate': this.value !== true && this.value !== false,
+      }
+    },
   },
   methods: {
     toggle(value) {
diff --git a/web-frontend/modules/core/pages/admin/settings.vue b/web-frontend/modules/core/pages/admin/settings.vue
index 51354e772..e40e74427 100644
--- a/web-frontend/modules/core/pages/admin/settings.vue
+++ b/web-frontend/modules/core/pages/admin/settings.vue
@@ -42,7 +42,6 @@
           <div class="admin-settings__control">
             <SwitchInput
               :value="settings.allow_new_signups"
-              :large="true"
               @input="updateSettings({ allow_new_signups: $event })"
               >{{ $t('settings.enabled') }}</SwitchInput
             >
@@ -66,7 +65,6 @@
           <div class="admin-settings__control">
             <SwitchInput
               :value="settings.allow_signups_via_workspace_invitations"
-              :large="true"
               @input="
                 updateSettings({
                   allow_signups_via_workspace_invitations: $event,
@@ -88,7 +86,6 @@
           <div class="admin-settings__control">
             <SwitchInput
               :value="settings.allow_reset_password"
-              :large="true"
               @input="updateSettings({ allow_reset_password: $event })"
               >{{ $t('settings.enabled') }}</SwitchInput
             >
@@ -113,7 +110,6 @@
           <div class="admin-settings__control">
             <SwitchInput
               :value="settings.allow_global_workspace_creation"
-              :large="true"
               @input="
                 updateSettings({ allow_global_workspace_creation: $event })
               "
@@ -177,7 +173,6 @@
           <div class="admin-settings__control">
             <SwitchInput
               :value="settings.track_workspace_usage"
-              :large="true"
               @input="updateSettings({ track_workspace_usage: $event })"
               >{{ $t('settings.enabled') }}</SwitchInput
             >
diff --git a/web-frontend/modules/core/pages/styleGuide.vue b/web-frontend/modules/core/pages/styleGuide.vue
index e103b704d..6c6bacc0d 100644
--- a/web-frontend/modules/core/pages/styleGuide.vue
+++ b/web-frontend/modules/core/pages/styleGuide.vue
@@ -267,19 +267,41 @@
             <div class="control__elements">
               value: {{ switchValue }}
               <br />
-              <SwitchInput v-model="switchValue"></SwitchInput>
-              <SwitchInput v-model="switchValue">With text</SwitchInput>
-              <SwitchInput v-model="switchUnknown">With text</SwitchInput>
-              <SwitchInput v-model="switchUnknown" large>With text</SwitchInput>
-              <SwitchInput v-model="switchValue" large></SwitchInput>
-              <SwitchInput v-model="switchValue" large>
-                Large with text
+              <SwitchInput
+                v-model="switchValue"
+                class="margin-bottom-1"
+              ></SwitchInput>
+              <SwitchInput v-model="switchValue" class="margin-bottom-1"
+                >With text</SwitchInput
+              >
+              <SwitchInput v-model="switchUnknown" class="margin-bottom-1"
+                >With text</SwitchInput
+              >
+              <SwitchInput v-model="switchUnknown" small class="margin-bottom-1"
+                >Small With text</SwitchInput
+              >
+              <SwitchInput
+                v-model="switchValue"
+                class="margin-bottom-1"
+                small
+              ></SwitchInput>
+              <SwitchInput class="margin-bottom-1" v-model="switchValue" large>
+                Small with text
               </SwitchInput>
-              <SwitchInput v-model="switchUnknown" disabled>
+              <SwitchInput
+                class="margin-bottom-1"
+                v-model="switchUnknown"
+                disabled
+              >
                 Disabled
               </SwitchInput>
-              <SwitchInput v-model="switchUnknown" disabled large>
-                Large disabled
+              <SwitchInput
+                class="margin-bottom-1"
+                v-model="switchUnknown"
+                disabled
+                small
+              >
+                Small disabled
               </SwitchInput>
             </div>
           </div>
diff --git a/web-frontend/modules/database/components/settings/APIToken.vue b/web-frontend/modules/database/components/settings/APIToken.vue
index 7300da77d..1ee1fc05e 100644
--- a/web-frontend/modules/database/components/settings/APIToken.vue
+++ b/web-frontend/modules/database/components/settings/APIToken.vue
@@ -122,9 +122,10 @@
           :key="operation"
           class="api-token__permission"
         >
-          <span>{{ operationName }}</span>
+          <span class="margin-bottom-1">{{ operationName }}</span>
           <SwitchInput
             :value="isActive(operation)"
+            small
             @input="toggle(operation, $event)"
           ></SwitchInput>
         </div>
@@ -144,6 +145,7 @@
             >
               <SwitchInput
                 :value="isDatabaseActive(database, operation)"
+                small
                 @input="toggleDatabase(database, databases, operation, $event)"
               ></SwitchInput>
             </div>
diff --git a/web-frontend/modules/database/components/settings/APITokenSettings.vue b/web-frontend/modules/database/components/settings/APITokenSettings.vue
index 6a865ff5d..9216d7581 100644
--- a/web-frontend/modules/database/components/settings/APITokenSettings.vue
+++ b/web-frontend/modules/database/components/settings/APITokenSettings.vue
@@ -21,13 +21,13 @@
           @deleted="deleteToken(token.id)"
         ></APIToken>
         <div v-if="tokens.length > 0" class="margin-top-3">
-          <SwitchInput :value="true" class="switch--static">
+          <SwitchInput :value="true" small class="margin-bottom-1">
             {{ $t('apiTokenSettings.hasFullPermissions') }}
           </SwitchInput>
-          <SwitchInput :value="2" class="switch--static">
+          <SwitchInput :value="2" small class="margin-bottom-1">
             {{ $t('apiTokenSettings.hasOnlySelectedPermissions') }}
           </SwitchInput>
-          <SwitchInput :value="false" class="switch--static">
+          <SwitchInput :value="false" small>
             {{ $t('apiTokenSettings.noPermissions') }}
           </SwitchInput>
         </div>
diff --git a/web-frontend/modules/database/components/view/ShareViewLink.vue b/web-frontend/modules/database/components/view/ShareViewLink.vue
index 1f948d83c..925d7cf0e 100644
--- a/web-frontend/modules/database/components/view/ShareViewLink.vue
+++ b/web-frontend/modules/database/components/view/ShareViewLink.vue
@@ -62,6 +62,8 @@
         <div class="view-sharing__shared-link-options">
           <div class="view-sharing__option">
             <SwitchInput
+              class="margin-bottom-1"
+              small
               :value="view.public_view_has_password"
               @input="toggleShareViewPassword"
             >
diff --git a/web-frontend/modules/database/components/view/ViewFieldsContext.vue b/web-frontend/modules/database/components/view/ViewFieldsContext.vue
index daeaf176a..ff61719ff 100644
--- a/web-frontend/modules/database/components/view/ViewFieldsContext.vue
+++ b/web-frontend/modules/database/components/view/ViewFieldsContext.vue
@@ -51,7 +51,7 @@
       </div>
     </div>
     <div v-auto-overflow-scroll class="hidings__body">
-      <ul class="hidings__list margin-bottom-0">
+      <ul class="hidings__list margin-top-1 margin-bottom-0">
         <li
           v-for="field in filteredFields"
           :key="field.id"
@@ -65,6 +65,7 @@
           <a class="hidings__item-handle" data-field-handle></a>
           <SwitchInput
             v-if="allowHidingFields"
+            small
             :value="!isHidden(field.id)"
             @input="updateFieldOptionsOfField(field, { hidden: !$event })"
           >
diff --git a/web-frontend/modules/database/components/view/ViewFilterForm.vue b/web-frontend/modules/database/components/view/ViewFilterForm.vue
index d1598f7ba..c6eb265ad 100644
--- a/web-frontend/modules/database/components/view/ViewFilterForm.vue
+++ b/web-frontend/modules/database/components/view/ViewFilterForm.vue
@@ -42,6 +42,7 @@
       </div>
       <div v-if="view.filters.length > 0">
         <SwitchInput
+          small
           :value="view.filters_disabled"
           @input="updateView(view, { filters_disabled: $event })"
           >{{ $t('viewFilterContext.disableAllFilters') }}</SwitchInput
diff --git a/web-frontend/modules/database/components/view/ViewSearchContext.vue b/web-frontend/modules/database/components/view/ViewSearchContext.vue
index 2dd518aaa..34d18dad7 100644
--- a/web-frontend/modules/database/components/view/ViewSearchContext.vue
+++ b/web-frontend/modules/database/components/view/ViewSearchContext.vue
@@ -6,7 +6,7 @@
     @shown="focus"
   >
     <form class="context__form" @submit.prevent="searchIfChanged">
-      <div class="control margin-bottom-1">
+      <div class="control margin-bottom-2">
         <div class="control__elements">
           <div
             class="input__with-icon input__with-icon--left"
@@ -30,6 +30,7 @@
       >
         <SwitchInput
           v-model="hideRowsNotMatchingSearch"
+          small
           @input="searchIfChanged"
         >
           {{ $t('viewSearchContext.hideNotMatching') }}
diff --git a/web-frontend/modules/database/components/view/form/FormViewField.vue b/web-frontend/modules/database/components/view/form/FormViewField.vue
index 855a08579..9cc7823dd 100644
--- a/web-frontend/modules/database/components/view/form/FormViewField.vue
+++ b/web-frontend/modules/database/components/view/form/FormViewField.vue
@@ -102,6 +102,8 @@
             </div>
           </div>
           <SwitchInput
+            class="margin-bottom-1"
+            small
             :value="fieldOptions.required"
             :disabled="readOnly"
             @input="$emit('updated-field-options', { required: $event })"
@@ -109,6 +111,7 @@
           >
           <SwitchInput
             v-if="allowedConditionalFields.length > 0"
+            small
             :value="fieldOptions.show_when_matching_conditions"
             :disabled="readOnly"
             @input="setShowWhenMatchingConditions($event)"
diff --git a/web-frontend/modules/database/components/view/form/FormViewMetaControls.vue b/web-frontend/modules/database/components/view/form/FormViewMetaControls.vue
index 80408f8c3..384e78338 100644
--- a/web-frontend/modules/database/components/view/form/FormViewMetaControls.vue
+++ b/web-frontend/modules/database/components/view/form/FormViewMetaControls.vue
@@ -12,7 +12,7 @@
       class="control form-view__control-notification-on-submit"
     >
       <SwitchInput
-        class=""
+        small
         :value="view.receive_notification_on_submit"
         @input="
           $emit('updated-form', { receive_notification_on_submit: $event })
diff --git a/web-frontend/stories/SwitchInput.stories.mdx b/web-frontend/stories/SwitchInput.stories.mdx
new file mode 100644
index 000000000..0aa0ecd81
--- /dev/null
+++ b/web-frontend/stories/SwitchInput.stories.mdx
@@ -0,0 +1,99 @@
+import { Meta, Story, Props, Canvas } from '@storybook/addon-docs/blocks'
+import { config, withDesign } from 'storybook-addon-designs'
+import { action } from '@storybook/addon-actions'
+import { useArgs } from '@storybook/client-api'
+
+import SwitchInput from '@baserow/modules/core/components/SwitchInput'
+
+<Meta
+    title="Baserow/Form/Switch"
+    component={SwitchInput}
+    parameters={{
+        backgrounds: {
+            default: 'white',
+            values: [
+                { name: 'white', value: '#ffffff' },
+                { name: 'light', value: '#eeeeee' },
+                { name: 'dark', value: '#222222' },
+            ],
+        },
+    }}
+    decorators={[
+        withDesign,
+        (story, context) => {
+            const [_, updateArgs] = useArgs()
+            return story({ ...context, updateArgs })
+        },
+    ]}
+    argTypes={{
+        default: {
+            defaultValue: 'Label',
+        },
+        value: {
+            control: {
+                type: 'radio',
+                options: [2, true, false],
+            },
+            defaultValue: false,
+        },
+        disabled: {
+            control: {
+                type: 'boolean',
+                options: [true, false],
+            },
+            defaultValue: false,
+        },
+        small: {
+            control: {
+                type: 'boolean',
+                options: [true, false],
+            },
+            defaultValue: false,
+        },
+    }}
+/>
+
+# Switch Input
+
+The switch input is a simple checkbox that can be used to toggle a value. It has also an intermediate state that can be used to indicate that the value is not yet known.
+
+export const Template = (args, { argTypes, updateArgs }) => ({
+    methods: {
+        action,
+        handleInput(checkboxValue) {
+            const value = checkboxValue
+            updateArgs({ ...args, value })
+            action('handleInput')(checkboxValue)
+        },
+    },
+    components: { SwitchInput },
+    props: Object.keys(argTypes),
+    template: `
+    <SwitchInput v-bind="$props" @input="handleInput">${args.default}</SwitchInput>`,
+})
+
+export const designConfig = {
+    type: 'figma',
+    url: 'https://www.figma.com/file/W7R2rQW7ohsZMeHRfEcPFW/Design-Library?node-id=1%3A89&mode=dev',
+}
+
+<Canvas>
+    <Story
+        name="Switch"
+        parameters={{
+            design: config(designConfig),
+        }}
+    >
+        {Template.bind({})}
+    </Story>
+</Canvas>
+
+## Example
+
+```javascript
+<SwitchInput></SwitchInput>
+```
+
+## Props
+
+<Props of={SwitchInput} />
diff --git a/web-frontend/test/unit/core/components/switchInput.spec.js b/web-frontend/test/unit/core/components/switchInput.spec.js
new file mode 100644
index 000000000..36bbfcef1
--- /dev/null
+++ b/web-frontend/test/unit/core/components/switchInput.spec.js
@@ -0,0 +1,70 @@
+import { mount } from '@vue/test-utils'
+import SwitchInput from '@baserow/modules/core/components/SwitchInput.vue'
+
+describe('SwitchInput', () => {
+  it('emits an input event when clicked', async () => {
+    const wrapper = mount(SwitchInput, {
+      propsData: {
+        checked: false,
+      },
+    })
+
+    await wrapper.find('.switch').trigger('click')
+
+    expect(wrapper.emitted('input')).toBeTruthy()
+    expect(wrapper.emitted('input')[0]).toEqual([true])
+  })
+
+  it('does not emit an input event when clicked and disabled', async () => {
+    const wrapper = mount(SwitchInput, {
+      propsData: {
+        value: false,
+        disabled: true,
+      },
+    })
+
+    await wrapper.find('.switch').trigger('click')
+
+    expect(wrapper.emitted('input')).toBeFalsy()
+  })
+
+  it('renders a small style when small prop is true', () => {
+    const wrapper = mount(SwitchInput, {
+      propsData: {
+        small: true,
+      },
+    })
+
+    expect(wrapper.find('.switch--small').exists()).toBe(true)
+  })
+
+  it('renders a disabled style when disabled prop is true', () => {
+    const wrapper = mount(SwitchInput, {
+      propsData: {
+        disabled: true,
+      },
+    })
+
+    expect(wrapper.find('.switch--disabled').exists()).toBe(true)
+  })
+
+  it('renders an active style when value prop is true', () => {
+    const wrapper = mount(SwitchInput, {
+      propsData: {
+        value: true,
+      },
+    })
+
+    expect(wrapper.find('.switch--active').exists()).toBe(true)
+  })
+
+  it('renders an inderterminate style when value prop is neither true or false', () => {
+    const wrapper = mount(SwitchInput, {
+      propsData: {
+        value: 4,
+      },
+    })
+
+    expect(wrapper.find('.switch--indeterminate').exists()).toBe(true)
+  })
+})
diff --git a/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap b/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap
index 47d013e46..0bac0a397 100644
--- a/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap
+++ b/web-frontend/test/unit/database/components/view/__snapshots__/viewFilterForm.spec.js.snap
@@ -1046,13 +1046,13 @@ exports[`ViewFilterForm component Full view filter component 1`] = `
      
     <div>
       <div
-        class="switch switch--has-content"
+        class="switch switch--small"
       >
-        <label
+        <div
           class="switch__label"
         >
           viewFilterContext.disableAllFilters
-        </label>
+        </div>
       </div>
     </div>
   </div>
@@ -1468,13 +1468,13 @@ exports[`ViewFilterForm component Test rating filter 1`] = `
      
     <div>
       <div
-        class="switch switch--has-content"
+        class="switch switch--small"
       >
-        <label
+        <div
           class="switch__label"
         >
           viewFilterContext.disableAllFilters
-        </label>
+        </div>
       </div>
     </div>
   </div>
@@ -1890,13 +1890,13 @@ exports[`ViewFilterForm component Test rating filter 2`] = `
      
     <div>
       <div
-        class="switch switch--has-content"
+        class="switch switch--small"
       >
-        <label
+        <div
           class="switch__label"
         >
           viewFilterContext.disableAllFilters
-        </label>
+        </div>
       </div>
     </div>
   </div>