Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/code-format.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs

name: Code format

on: pull_request

jobs:
code-format:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
cache: 'npm'
- run: npm ci
- run: npm run format-check
4 changes: 2 additions & 2 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
"printWidth": 100,
"trailingComma": "es5"
}
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# client

This template should help get you started developing with Vue 3 in Vite.
This is the client SPA for DomoticASW, it is deployed as a submodule inside DomoticASW's server.

## Recommended IDE Setup

Expand All @@ -14,9 +14,10 @@ TypeScript cannot handle type information for `.vue` imports by default, so we r

See [Vite Configuration Reference](https://vite.dev/config/).

## Project Setup
## Setup

```sh
./setup.sh
npm install
```

Expand Down
14 changes: 14 additions & 0 deletions hooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/bin/sh
#
# An example hook script to verify what is about to be committed.
# Called by "git commit" with no arguments. The hook should
# exit with non-zero status after issuing an appropriate message if
# it wants to stop the commit.

# Redirect output to stderr.
exec 1>&2

if ! npm run format-check; then
echo Run \"npm run format\" to fix
exit 1
fi
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"type-check": "vue-tsc --build",
"lint": "eslint .",
"lint-fix": "npm run lint --fix",
"format": "prettier --write src/"
"format": "prettier --write src/",
"format-check": "prettier --check src/"
},
"dependencies": {
"@tailwindcss/vite": "^4.1.11",
Expand Down
16 changes: 7 additions & 9 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ import DeviceGroupsDialog from './components/DeviceGroupsDialog.vue'
<template>
<div class="flex justify-center">
<ErrorPresenter>
<LoadingOverlay>
<div class="h-full w-full max-w-5xl">
<RouterView />
<SuccessPresenter>
</SuccessPresenter>
<DeviceGroupsDialog/>
</div>
</LoadingOverlay>

<LoadingOverlay>
<div class="h-full w-full max-w-5xl">
<RouterView />
<SuccessPresenter> </SuccessPresenter>
<DeviceGroupsDialog />
</div>
</LoadingOverlay>
</ErrorPresenter>
</div>
</template>
Expand Down
23 changes: 11 additions & 12 deletions src/api/Deserializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export type Deserializer<T> = (json: unknown) => T
export function Deserializer<From, To>(
checkDeserializable: (obj: unknown) => obj is From,
deserialize: (obj: From) => To,
deserializationError?: (obj: unknown) => string): Deserializer<To> {

deserializationError?: (obj: unknown) => string
): Deserializer<To> {
return (json) => {
if (checkDeserializable(json)) {
return deserialize(json)
Expand All @@ -29,19 +29,18 @@ export function arrayDeserializer<T>(itemDeserializer: Deserializer<T>): Deseria
if (!Array.isArray(obj)) {
throw DeserializeError(`Expecting an array but ${typeof obj} was found`)
}
return obj.map(item => itemDeserializer(item))
return obj.map((item) => itemDeserializer(item))
}
}

function isBoolean(o: unknown): o is boolean {
return o != undefined && typeof o == "boolean"
return o != undefined && typeof o == 'boolean'
}
export const booleanDeserializer: Deserializer<boolean> =
Deserializer(
isBoolean,
(obj) => obj,
(obj) => `Expecting a boolean but ${typeof obj} was found`
)
export const booleanDeserializer: Deserializer<boolean> = Deserializer(
isBoolean,
(obj) => obj,
(obj) => `Expecting a boolean but ${typeof obj} was found`
)

export interface DeserializeError {
message: string
Expand All @@ -50,7 +49,7 @@ export interface DeserializeError {

export function DeserializeError(cause?: string): DeserializeError {
return {
message: "There was an error while deserializing a response from the server.",
cause
message: 'There was an error while deserializing a response from the server.',
cause,
}
}
16 changes: 7 additions & 9 deletions src/api/IdDTO.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
import { Deserializer } from "./Deserializer";
import { Deserializer } from './Deserializer'

export interface IdDTO {
id: string
}

export function isIdDTO(o: unknown): o is IdDTO {
return o != undefined && typeof o == "object" &&
"id" in o && typeof o.id == "string"
return o != undefined && typeof o == 'object' && 'id' in o && typeof o.id == 'string'
}

export const idDeserializer =
Deserializer<IdDTO, string>(
isIdDTO,
(dto) => dto.id,
(obj) => `Unable to deserialize ${obj} into a string id since it was not an IdDTO`
)
export const idDeserializer = Deserializer<IdDTO, string>(
isIdDTO,
(dto) => dto.id,
(obj) => `Unable to deserialize ${obj} into a string id since it was not an IdDTO`
)
13 changes: 9 additions & 4 deletions src/api/ServerError.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Deserializer } from "./Deserializer"
import { Deserializer } from './Deserializer'

export interface ServerError {
__brand: string
Expand All @@ -7,9 +7,14 @@ export interface ServerError {
}

export function isServerError(o: unknown): o is ServerError {
return o != undefined && typeof o == "object" &&
"__brand" in o && typeof o.__brand == "string" &&
"message" in o && typeof o.message == "string"
return (
o != undefined &&
typeof o == 'object' &&
'__brand' in o &&
typeof o.__brand == 'string' &&
'message' in o &&
typeof o.message == 'string'
)
}

export const toServerErrorDeserializer: Deserializer<ServerError> = Deserializer(
Expand Down
22 changes: 13 additions & 9 deletions src/api/api.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { DeserializeError, type Deserializer } from "./Deserializer"
import { toServerErrorDeserializer } from "./ServerError"
import { DeserializeError, type Deserializer } from './Deserializer'
import { toServerErrorDeserializer } from './ServerError'

/**
* Does the same as `request` plus setting the Authorization header to token if it is not already set.
*/
export async function authorizedRequest(url: RequestInfo | URL, token: string, init?: RequestInit): Promise<Response> {
export async function authorizedRequest(
url: RequestInfo | URL,
token: string,
init?: RequestInit
): Promise<Response> {
const headers = new Headers(init?.headers) // Ensuring that the type of init.headers is Headers
if (!headers.has("Authorization")) {
headers.append("Authorization", token)
if (!headers.has('Authorization')) {
headers.append('Authorization', token)
}
return await request(url, { ...init, headers })
}
Expand All @@ -21,8 +25,8 @@ export async function authorizedRequest(url: RequestInfo | URL, token: string, i
*/
export async function request(url: RequestInfo | URL, init?: RequestInit): Promise<Response> {
const headers = new Headers(init?.headers) // Ensuring that the type of init.headers is Headers
if (!headers.has("Content-Type")) {
headers.append("Content-Type", "application/json")
if (!headers.has('Content-Type')) {
headers.append('Content-Type', 'application/json')
}
const response = await fetch(url, { ...init, headers })
if (!response.ok) {
Expand All @@ -41,14 +45,14 @@ export async function deserializeBody<T>(res: Response, deserializer: Deserializ
// Passing through a text representation as res.json() uses JSON.parse and will fail in case of empty body
const bodyAsText = await res.text()
if (bodyAsText.trim().length == 0) {
throw DeserializeError("Response body was empty")
throw DeserializeError('Response body was empty')
}

let body: object
try {
body = JSON.parse(bodyAsText)
} catch (e) {
throw DeserializeError("Unable to parse json from response body.\n" + (e as Error).message)
throw DeserializeError('Unable to parse json from response body.\n' + (e as Error).message)
}

if (!res.ok) {
Expand Down
40 changes: 23 additions & 17 deletions src/api/devices-management/dtos/device-groups/DeviceGroupDTO.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { DeviceGroupId, type DeviceGroup } from "@/model/devices-management/DeviceGroup"
import { Deserializer } from "../../../Deserializer"
import { deviceDeserializer, isDeviceDTO, type DeviceDTO } from "../devices/DeviceDTO"
import { DeviceGroupId, type DeviceGroup } from '@/model/devices-management/DeviceGroup'
import { Deserializer } from '../../../Deserializer'
import { deviceDeserializer, isDeviceDTO, type DeviceDTO } from '../devices/DeviceDTO'

export interface DeviceGroupDTO {
id: string
Expand All @@ -9,19 +9,25 @@ export interface DeviceGroupDTO {
}

export function isDeviceGroupDTO(o: unknown): o is DeviceGroupDTO {
return o != undefined && typeof o === "object" &&
"id" in o && typeof o.id === "string" &&
"name" in o && typeof o.name === "string" &&
"devices" in o && Array.isArray(o.devices) && o.devices.every(isDeviceDTO)
return (
o != undefined &&
typeof o === 'object' &&
'id' in o &&
typeof o.id === 'string' &&
'name' in o &&
typeof o.name === 'string' &&
'devices' in o &&
Array.isArray(o.devices) &&
o.devices.every(isDeviceDTO)
)
}

export const deviceGroupDeserializer =
Deserializer<DeviceGroupDTO, DeviceGroup>(
isDeviceGroupDTO,
(dto) => ({
id: DeviceGroupId(dto.id),
name: dto.name,
devices: dto.devices.map(deviceDeserializer)
}),
(obj) => `Unable to parse ${obj} into a DeviceGroup since it was not a DeviceGroupDTO`
)
export const deviceGroupDeserializer = Deserializer<DeviceGroupDTO, DeviceGroup>(
isDeviceGroupDTO,
(dto) => ({
id: DeviceGroupId(dto.id),
name: dto.name,
devices: dto.devices.map(deviceDeserializer),
}),
(obj) => `Unable to parse ${obj} into a DeviceGroup since it was not a DeviceGroupDTO`
)
36 changes: 20 additions & 16 deletions src/api/devices-management/dtos/devices/ColorDTO.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@

import { Color } from "@/model/devices-management/Types";
import { Deserializer } from "../../../Deserializer";
import { Color } from '@/model/devices-management/Types'
import { Deserializer } from '../../../Deserializer'

export interface ColorDTO {
readonly r: number;
readonly g: number;
readonly b: number;
readonly r: number
readonly g: number
readonly b: number
}

export function isColorDTO(o: unknown): o is ColorDTO {
return o != undefined && typeof o == "object" &&
"r" in o && typeof o.r == "number" &&
"g" in o && typeof o.g == "number" &&
"b" in o && typeof o.b == "number"
return (
o != undefined &&
typeof o == 'object' &&
'r' in o &&
typeof o.r == 'number' &&
'g' in o &&
typeof o.g == 'number' &&
'b' in o &&
typeof o.b == 'number'
)
}

export const colorDeserializer =
Deserializer<ColorDTO, Color>(
isColorDTO,
(dto) => Color(dto.r, dto.g, dto.b),
(obj) => `Unable to deserialize ${obj} into a Color since it was not a ColorDTO`
)
export const colorDeserializer = Deserializer<ColorDTO, Color>(
isColorDTO,
(dto) => Color(dto.r, dto.g, dto.b),
(obj) => `Unable to deserialize ${obj} into a Color since it was not a ColorDTO`
)
48 changes: 28 additions & 20 deletions src/api/devices-management/dtos/devices/DeviceActionDTO.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { DeviceActionId, type DeviceAction } from "@/model/devices-management/Device"
import { Deserializer } from "../../../Deserializer"
import { isTypeConstraintsDTO, typeConstraintsDeserializer, type TypeConstraintsDTO } from "./TypeConstraintsDTO"
import { DeviceActionId, type DeviceAction } from '@/model/devices-management/Device'
import { Deserializer } from '../../../Deserializer'
import {
isTypeConstraintsDTO,
typeConstraintsDeserializer,
type TypeConstraintsDTO,
} from './TypeConstraintsDTO'

export interface DeviceActionDTO {
readonly id: string
Expand All @@ -10,22 +14,26 @@ export interface DeviceActionDTO {
}

export function isDeviceActionDTO(o: unknown): o is DeviceActionDTO {
return o != undefined && typeof o == "object" &&
"id" in o && typeof o.id == "string" &&
"name" in o && typeof o.name == "string" &&
("description" in o ? typeof o.description == "string" : true) &&
"inputTypeConstraints" in o && isTypeConstraintsDTO(o.inputTypeConstraints)
return (
o != undefined &&
typeof o == 'object' &&
'id' in o &&
typeof o.id == 'string' &&
'name' in o &&
typeof o.name == 'string' &&
('description' in o ? typeof o.description == 'string' : true) &&
'inputTypeConstraints' in o &&
isTypeConstraintsDTO(o.inputTypeConstraints)
)
}


export const deviceActionDeserializer =
Deserializer<DeviceActionDTO, DeviceAction<unknown>>(
isDeviceActionDTO,
(dto) => ({
id: DeviceActionId(dto.id),
name: dto.name,
description: dto.description,
inputTypeConstraints: typeConstraintsDeserializer(dto.inputTypeConstraints)
}),
(obj) => `Unable to deserialize ${obj} into a DeviceAction since it was not a DeviceActionDTO`
)
export const deviceActionDeserializer = Deserializer<DeviceActionDTO, DeviceAction<unknown>>(
isDeviceActionDTO,
(dto) => ({
id: DeviceActionId(dto.id),
name: dto.name,
description: dto.description,
inputTypeConstraints: typeConstraintsDeserializer(dto.inputTypeConstraints),
}),
(obj) => `Unable to deserialize ${obj} into a DeviceAction since it was not a DeviceActionDTO`
)
Loading