diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 4f7c3b1..3a2c28d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -9,6 +9,7 @@ "core:default", "opener:default", "dialog:default", + "fs:allow-write-text-file", "fs:default" ] -} \ No newline at end of file +} diff --git a/src/App.vue b/src/App.vue index 656184c..4018b86 100644 --- a/src/App.vue +++ b/src/App.vue @@ -36,4 +36,9 @@ main <style lang="sass"> header margin-bottom: 1rem +.ellipsis + display: inline-block + overflow: hidden + text-overflow: ellipsis + white-space: nowrap </style> diff --git a/src/main.ts b/src/main.ts index 8bc77e7..e28cd7b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,6 +4,7 @@ import { definePreset } from '@primeuix/themes' import Aura from '@primeuix/themes/aura' import PrimeVue from 'primevue/config' import ToastService from 'primevue/toastservice' +import Tooltip from 'primevue/tooltip' import 'primeicons/primeicons.css' import { router } from './router' @@ -12,7 +13,6 @@ import App from './App.vue' const pinia = createPinia() const app = createApp(App) - const themePreset = definePreset(Aura, { semantic: { primary: { @@ -44,4 +44,5 @@ app }, }) .use(ToastService) + .directive('tooltip', Tooltip) .mount('#app') diff --git a/src/store.ts b/src/store.ts index f7f90f1..90754ad 100644 --- a/src/store.ts +++ b/src/store.ts @@ -17,12 +17,14 @@ export const useAppStore = defineStore('appStore', { } as InventoryState, }), getters: { - fileTree: (state) => fileTree(state.currentInventory), - fileDirectory: (state) => { + fileTree(state) { + return fileTree(state.currentInventory) + }, + fileDirectory(state) { const split = fileTree(state.currentInventory) return state.currentInventory.filePath.replace(split[split.length - 1], '') }, - fileName: (state) => { + fileName(state) { const split = fileTree(state.currentInventory) return split[split.length - 1] }, diff --git a/src/views/Editor/DataEditor.vue b/src/views/Editor/DataEditor.vue index 1d46202..c80eb51 100644 --- a/src/views/Editor/DataEditor.vue +++ b/src/views/Editor/DataEditor.vue @@ -15,35 +15,40 @@ import { const props = defineProps<{ columns: DataColumn[], }>() -const model = defineModel<DataRow[]>() +const emits = defineEmits<{ + (e: 'dirty'): void +}>() +const model = defineModel<DataRow[]>({ required: true }) const editingRows = ref([]) const onRowEditSave = (event: { newData: any, index: number }) => { let { newData, index } = event - model.value![index] = newData + model.value[index] = newData + emits('dirty') } const onAddRow = (event: Event) => { event.preventDefault() - model.value!.push({ _id: uuidv7() }) - console.log(model.value) + model.value.push({ _id: uuidv7() }) + emits('dirty') requestAnimationFrame(() => { - (document.querySelector('tr:last-of-type button.edit') as HTMLButtonElement).click() + (document.querySelector('.data-editor tr:last-of-type button.edit') as HTMLButtonElement).click() requestAnimationFrame(() => { - (document.querySelector('tr:last-of-type input') as HTMLInputElement | undefined)?.focus() + (document.querySelector('.data-editor tr:last-of-type input') as HTMLInputElement | undefined)?.select() }) }) } const onDeleteRow = (event: Event, slotProps: { index: number }) => { event.preventDefault() - model.value!.splice(slotProps.index, 1) + model.value.splice(slotProps.index, 1) + emits('dirty') } </script> <template lang="pug"> -DataTable( +DataTable.data-editor( @row-edit-save='onRowEditSave' v-model:editingRows='editingRows' :value='model' @@ -93,6 +98,7 @@ DataTable( ) Column( rowEditor + header='Actions' style='width:10%;min-width:8rem;' bodyStyle='text-align:center;' ) @@ -157,4 +163,7 @@ Button( :deep(.p-image) img max-width: 16rem +:deep(td:last-of-type) + display: flex + padding-left: 8px </style> diff --git a/src/views/Editor/Editor.vue b/src/views/Editor/Editor.vue index 17659f2..784a3ca 100644 --- a/src/views/Editor/Editor.vue +++ b/src/views/Editor/Editor.vue @@ -1,5 +1,12 @@ <script setup lang="ts"> -import { ref } from 'vue' +import { + computed, + ref, +} from 'vue' +import { useRouter } from 'vue-router' +import { useToast } from 'primevue' +import { writeTextFile } from '@tauri-apps/plugin-fs' +import Menubar from 'primevue/menubar' import Tabs from 'primevue/tabs' import TabList from 'primevue/tablist' import Tab from 'primevue/tab' @@ -14,13 +21,71 @@ import DataEditor from './DataEditor.vue' import FieldEditor from './FieldEditor.vue' const appStore = useAppStore() +const router = useRouter() +const toast = useToast() -const data = ref([...appStore.currentInventory.data?.rows || []]) -const fields = ref([...appStore.currentInventory.data?.columns || []]) +const data = ref(appStore.currentInventory.data?.rows) +const fields = ref(appStore.currentInventory.data?.columns) +const dirty = ref(false) +const fileDirectory = ref(appStore.fileDirectory) +const fileName = ref(appStore.fileName) + +const onSave = async () => { + try { + await writeTextFile(appStore.currentInventory.filePath, JSON.stringify({ + rows: data.value, + columns: fields.value, + }, null, 2)) + } catch (err) { + toast.add({ + severity: 'error', + summary: 'The file could not be saved (permission issue?)', + detail: err, + }) + } + dirty.value = false +} + +const onQuit = () => { + // todo: should prompt to confirm + router.push('/') +} + +const menu = computed(() => ([ + { + label: 'Save', + icon: 'pi pi-save', + command: onSave, + disabled: !dirty.value, + }, + { + label: 'Exit', + icon: 'pi pi-sign-out', + command: onQuit, + }, +])) + +const onDirty = () => { + dirty.value = true +} </script> <template lang="pug"> -Tabs(value='data-editor') +Menubar( + :model='menu' +) + template(#end) + .context + span Editing for + span.ellipsis( + v-tooltip.bottom='fileDirectory' + ) + span {{ fileDirectory }} + span.ellipsis( + v-tooltip.bottom='fileName' + ) + span {{ fileName }} +Tabs.tabs(value='data-editor') TabList Tab(value='data-editor') Data Editor Tab(value='field-editor') Field Editor @@ -28,14 +93,22 @@ Tabs(value='data-editor') TabPanel(value='field-editor') FieldEditor( v-model='fields' + @dirty='onDirty' ) TabPanel(value='data-editor') DataEditor( v-model='data' :columns='fields' + @dirty='onDirty' ) </template> <style scoped lang="sass"> - +.tabs + margin-top: 1rem +.context + display: flex + gap: 1rem + .ellipsis + max-width: 8rem </style> diff --git a/src/views/Editor/FieldEditor.vue b/src/views/Editor/FieldEditor.vue index 6160d29..c0429cc 100644 --- a/src/views/Editor/FieldEditor.vue +++ b/src/views/Editor/FieldEditor.vue @@ -1,5 +1,6 @@ <script setup lang="ts"> import { ref } from 'vue' +import { v1 as uuidv1 } from 'uuid' import Button from 'primevue/button' import DataTable from 'primevue/datatable' import Column from 'primevue/column' @@ -11,18 +12,40 @@ import { type Column as DataColumn, } from 'src/types/data' -const model = defineModel<DataColumn[]>() +const emits = defineEmits<{ + (e: 'dirty'): void +}>() +const model = defineModel<DataColumn[]>({ required: true }) const editingFields = ref([]) const onFieldEditSave = (event: { newData: any, index: number }) => { let { newData, index } = event - model.value![index] = newData - console.log(model.value) + model.value[index] = newData + emits('dirty') +} + +const onAddField = (event: Event) => { + event.preventDefault() + model.value.push({ name: uuidv1().split('-')[0], type: 'text' }) + emits('dirty') + + requestAnimationFrame(() => { + (document.querySelector('.field-editor tr:last-of-type button.edit') as HTMLButtonElement).click() + requestAnimationFrame(() => { + (document.querySelector('.field-editor tr:last-of-type input') as HTMLInputElement | undefined)?.select() + }) + }) +} + +const onDeleteField = (event: Event, slotProps: { index: number }) => { + event.preventDefault() + model.value.splice(slotProps.index, 1) + emits('dirty') } </script> <template lang="pug"> -DataTable( +DataTable.field-editor( @row-edit-save='onFieldEditSave' v-model:editingRows='editingFields' :value='model' @@ -61,13 +84,14 @@ DataTable( ) Column( rowEditor + header='Actions' style='width:10%;min-width:8rem;' bodyStyle='text-align:center;' ) template( #body='slotProps' ) - Button( + Button.edit( icon='pi pi-pencil' aria-label='Row Edit' rounded @@ -83,6 +107,9 @@ DataTable( rounded severity='danger' variant='text' + :onClick=`(e) => { + onDeleteField(e, slotProps) + }` ) template( #editor='slotProps' @@ -107,7 +134,16 @@ DataTable( slotProps.editorCancelCallback(e) }` ) +Button( + icon='pi pi-plus' + aria-label='Add New Field' + label='Add New Field' + @click='onAddField' +) </template> <style scoped lang="sass"> +:deep(td:last-of-type) + display: flex + padding-left: 8px </style> diff --git a/src/views/Home/Home.vue b/src/views/Home/Home.vue index ee0248a..b2179d7 100644 --- a/src/views/Home/Home.vue +++ b/src/views/Home/Home.vue @@ -38,7 +38,7 @@ const onBrowse = async (e: Event) => { const onCreate = async (e: Event) => { e.preventDefault() - appStore.currentInventory.filePath = `${filePath}/${fileName}.json` + appStore.currentInventory.filePath = `${filePath.value}/${fileName.value}.json` appStore.currentInventory.data = { columns: [ { name: 'name', type: 'text' },