Skip to content

Commit

Permalink
Typescript support (#15)
Browse files Browse the repository at this point in the history
* add config for ts and webpack

* configure webpack and babel for typescript

* ci: set REUSE for json files

* ci(reuse): add composer.lock to dep5
  • Loading branch information
jabberwoc authored May 10, 2024
1 parent 902d086 commit 5eef2db
Show file tree
Hide file tree
Showing 10 changed files with 13,919 additions and 472 deletions.
5 changes: 3 additions & 2 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
module.exports = {
extends: [
'@nextcloud',
'@nextcloud/eslint-config/typescript',
],
rules: {
'n/no-missing-import': 'off',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': ['error'],
},
}
2 changes: 1 addition & 1 deletion .reuse/dep5
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Upstream-Name: Template App
Upstream-Contact: Sebastian Stöcker <[email protected]>
Source: https://github.com/nextcloud/profiler

Files: package-lock.json package.json composer.json composer.lock versions.json .github/renovate.json .vscode/*.json *.csv *.png
Files: *.json composer.lock .github/renovate.json .vscode/*.json *.csv *.png
Copyright: Sebastian Stöcker <[email protected]>
License: AGPL-3.0-or-later

Expand Down
9 changes: 9 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
const babelConfig = require('@nextcloud/babel-config')

babelConfig.presets = [...babelConfig.presets, '@babel/typescript']
babelConfig.plugins = [
...babelConfig.plugins,
'@babel/transform-typescript',
'@babel/proposal-class-properties',
'@babel/transform-async-to-generator',
'@babel/plugin-transform-object-rest-spread',
]

module.exports = babelConfig
14,206 changes: 13,796 additions & 410 deletions package-lock.json

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,21 @@
"license": "agpl",
"private": true,
"scripts": {
"build": "webpack --node-env production --progress",
"build": "npm run check && webpack --node-env production --progress",
"dev": "webpack --node-env development --progress",
"watch": "webpack --node-env development --progress --watch",
"serve": "webpack serve --node-env development --progress --allowed-hosts all",
"lint": "eslint --ext .js,.vue src",
"lint:fix": "eslint --ext .js,.vue src --fix",
"stylelint": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue",
"stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue --fix"
"stylelint:fix": "stylelint css/*.css css/*.scss src/**/*.scss src/**/*.vue --fix",
"check": "vue-tsc --noEmit"
},
"dependencies": {
"@nextcloud/axios": "^2.4.0",
"@nextcloud/dialogs": "^3.1.4",
"@nextcloud/router": "^2.0.0",
"@nextcloud/typings": "^1.8.0",
"@nextcloud/vue": "^8.7.1",
"vue": "^2.7.0"
},
Expand All @@ -34,13 +36,17 @@
"npm": "^7.0.0 || ^8.0.0"
},
"devDependencies": {
"@babel/preset-env": "^7.24.5",
"@babel/preset-typescript": "^7.24.1",
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.2.0",
"@nextcloud/eslint-config": "^8.3.0",
"@nextcloud/stylelint-config": "^2.1.2",
"@nextcloud/webpack-vue-config": "^5.2.1",
"@nextcloud/webpack-vue-config": "^5.5.1",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.56.0",
"vue-tsc": "^2.0.17",
"webpack": "^5.65.0",
"webpack-cli": "^5.0.1"
}
}
}
109 changes: 54 additions & 55 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,26 @@
<NcContent app-name="templateapp">
<NcAppNavigation>
<NcAppNavigationNew v-if="!loading"
:text="t('templateapp', 'New note')"
:text="'New note'"
:disabled="false"
button-id="new-templateapp-button"
button-class="icon-add"
@click="newNote" />
<ul>
<NcAppNavigationItem v-for="note in notes"
:key="note.title"
:name="note.title ? note.title : t('templateapp', 'New note')"
:title="note.title ? note.title : t('templateapp', 'New note')"
:class="{active: currentNoteId === note.id}"
:name="note.title ? note.title : 'New note'"
:title="note.title ? note.title : 'New note'"
:class="{ active: currentNoteId === note.id }"
@click="openNote(note)">
<template slot="actions">
<NcActionButton v-if="note.id === -1"
icon="icon-close"
@click="cancelNewNote(note)">
<NcActionButton v-if="note.id === -1" icon="icon-close" @click="cancelNewNote">
{{
t('templateapp', 'Cancel note creation') }}
'Cancel note creation' }}
</NcActionButton>
<NcActionButton v-else
icon="icon-delete"
@click="deleteNote(note)">
<NcActionButton v-else icon="icon-delete" @click="deleteNote(note)">
{{
t('templateapp', 'Delete note') }}
'Delete note' }}
</NcActionButton>
</template>
</NcAppNavigationItem>
Expand All @@ -44,30 +40,32 @@
<textarea ref="content" v-model="currentNote.content" :disabled="updating" />
<input type="button"
class="primary"
:value="t('templateapp', 'Save')"
:value="'Save'"
:disabled="updating || !savePossible"
@click="saveNote">
</div>
<div v-else id="emptycontent">
<div class="icon-file" />
<h2>
{{
t('templateapp', 'Create a note to get started') }}
'Create a note to get started' }}
</h2>
</div>
</NcAppContent>
</NcContent>
</template>

<script>
<script lang="ts">
import { NcContent, NcAppNavigation, NcAppNavigationItem, NcAppNavigationNew, NcAppContent, NcActionButton } from '@nextcloud/vue'
import '@nextcloud/dialogs/styles/toast.scss'
import { generateUrl } from '@nextcloud/router'
import { showError, showSuccess } from '@nextcloud/dialogs'
import axios from '@nextcloud/axios'
import Vue from 'vue'
import type { AppData, Note } from './model.ts'
export default {
export default Vue.extend({
name: 'App',
components: {
NcContent,
Expand All @@ -77,7 +75,7 @@ export default {
NcAppNavigationItem,
NcAppNavigationNew,
},
data() {
data(): AppData {
return {
notes: [],
currentNoteId: null,
Expand All @@ -90,19 +88,19 @@ export default {
* Return the currently selected note object
* @return {object|null}
*/
currentNote() {
currentNote(): Note | null {
if (this.currentNoteId === null) {
return null
}
return this.notes.find((note) => note.id === this.currentNoteId)
return this.notes.find((note) => note.id === this.currentNoteId) || null
},
/**
* Returns true if a note is selected and its title is not empty
* @return {boolean}
*/
savePossible() {
return this.currentNote && this.currentNote.title !== ''
savePossible(): boolean {
return this.currentNote != null && this.currentNote.title !== ''
},
},
/**
Expand All @@ -114,7 +112,7 @@ export default {
this.notes = response.data
} catch (e) {
console.error(e)
showError(t('notestutorial', 'Could not fetch notes'))
showError('Could not fetch notes')
}
this.loading = false
},
Expand All @@ -124,32 +122,32 @@ export default {
* Create a new note and focus the note content field automatically
* @param {object} note Note object
*/
openNote(note) {
openNote(note: Note): void {
if (this.updating) {
return
}
this.currentNoteId = note.id
this.$nextTick(() => {
this.$refs.content.focus()
(this.$refs.title as HTMLInputElement).focus()
})
},
/**
* Action triggered when clicking the save button
* create a new note or save
*/
saveNote() {
saveNote(): void {
if (this.currentNoteId === -1) {
this.createNote(this.currentNote)
this.createNote(this.currentNote as Note)
} else {
this.updateNote(this.currentNote)
this.updateNote(this.currentNote as Note)
}
},
/**
* Create a new note and focus the note content field automatically
* The note is not yet saved, therefore an id of -1 is used until it
* has been persisted in the backend
*/
newNote() {
newNote(): void {
if (this.currentNoteId !== -1) {
this.currentNoteId = -1
this.notes.push({
Expand All @@ -158,45 +156,45 @@ export default {
content: '',
})
this.$nextTick(() => {
this.$refs.title.focus()
(this.$refs.title as HTMLInputElement).focus()
})
}
},
/**
* Abort creating a new note
*/
cancelNewNote() {
cancelNewNote(): void {
this.notes.splice(this.notes.findIndex((note) => note.id === -1), 1)
this.currentNoteId = null
},
/**
* Create a new note by sending the information to the server
* @param {object} note Note object
*/
async createNote(note) {
async createNote(note: Note): Promise<void> {
this.updating = true
try {
const response = await axios.post(generateUrl('/apps/templateapp/notes'), note)
const index = this.notes.findIndex((match) => match.id === this.currentNoteId)
const index: number = this.notes.findIndex((match) => match.id === this.currentNoteId)
this.$set(this.notes, index, response.data)
this.currentNoteId = response.data.id
} catch (e) {
console.error(e)
showError(t('notestutorial', 'Could not create the note'))
showError('Could not create the note')
}
this.updating = false
},
/**
* Update an existing note on the server
* @param {object} note Note object
* @param {Note} note Note object
*/
async updateNote(note) {
async updateNote(note: Note): Promise<void> {
this.updating = true
try {
await axios.put(generateUrl(`/apps/templateapp/notes/${note.id}`), note)
} catch (e) {
console.error(e)
showError(t('notestutorial', 'Could not update the note'))
showError('Could not update the note')
}
this.updating = false
Expand All @@ -205,38 +203,39 @@ export default {
* Delete a note, remove it from the frontend and show a hint
* @param {object} note Note object
*/
async deleteNote(note) {
async deleteNote(note: Note): Promise<void> {
try {
await axios.delete(generateUrl(`/apps/templateapp/notes/${note.id}`))
this.notes.splice(this.notes.indexOf(note), 1)
if (this.currentNoteId === note.id) {
this.currentNoteId = null
}
showSuccess(t('templateapp', 'Note deleted'))
showSuccess('Note deleted')
} catch (e) {
console.error(e)
showError(t('templateapp', 'Could not delete the note'))
showError('Could not delete the note')
}
},
},
}
})
</script>
<style scoped>
#app-content > div {
width: 100%;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
#app-content>div {
width: 100%;
height: 100%;
padding: 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
}
input[type='text'] {
width: 100%;
}
input[type='text'] {
width: 100%;
}
textarea {
flex-grow: 1;
width: 100%;
}
textarea {
flex-grow: 1;
width: 100%;
}
</style>
15 changes: 15 additions & 0 deletions src/model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: Sebastian Stöcker <[email protected]>
// SPDX-License-Identifier: AGPL-3.0-or-later

export interface Note {
id: number
title: string
content: string
}

export interface AppData {
notes: Array<Note>
currentNoteId: number | null
updating: boolean
loading: boolean
}
24 changes: 24 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["./src/**/*.ts", "./src/**/*.vue", "./*.d.ts"],
"compilerOptions": {
"types": ["jest", "node", "vue", "vue-router", "@nextcloud/typings"],
"target": "ESNext",
"module": "esnext",
// Set module resolution to bundler and `noEmit` to be able to set `allowImportingTsExtensions`, so we can import Typescript with .ts extension
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
// "emitDeclarationOnly": true,
"noEmit": true,
// Allow ts to import js files
"allowJs": true,
"allowSyntheticDefaultImports": true,
"declaration": false,
"noImplicitAny": false,
"resolveJsonModule": true,
"strict": true,
"sourceMap": true,
"isolatedModules": true
},
"files": ["./vue-shims.d.ts"]
}
6 changes: 6 additions & 0 deletions vue-shims.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// SPDX-FileCopyrightText: Sebastian Stöcker <[email protected]>
// SPDX-License-Identifier: AGPL-3.0-or-later
declare module "*.vue" {
import Vue from "vue"
export default Vue
}
Loading

0 comments on commit 5eef2db

Please sign in to comment.