From 863422df8e21c33ad7469c33ccea070b2b0d2fd2 Mon Sep 17 00:00:00 2001 From: seb Date: Tue, 3 Nov 2026 06:46:36 +0100 Subject: [PATCH] u --- src/components/carrier/CarrierItem.jsx | 5 +++ src/components/kunde/KundeGruppe.jsx | 2 + .../settings/BackupSettingsPanel.jsx | 17 +++++++-- .../settings/CompanySettingsPanel.jsx | 18 ++++++--- .../settings/ImportExportSettingsPanel.jsx | 18 +++++++-- .../settings/NumberSettingsPanel.jsx | 13 ++++--- .../settings/PosTerminalSettingsPanel.jsx | 30 +++++++++------ .../settings/PrinterAgentSettingsPanel.jsx | 27 ++++++++------ .../settings/PrinterSettingsPanel.jsx | 26 ++++++++----- src/components/settings/SettingsKeysPanel.jsx | 15 ++++++-- src/components/vfs/VfsEditorPanel.jsx | 14 ++++++- src/components/vfs/VfsFileExplorer.jsx | 37 ++++++++++++++++++- src/components/vfs/VfsNewFileDialog.jsx | 29 +++++++++++++-- src/components/vfs/VfsRenameDialog.jsx | 30 +++++++++++++-- src/components/vfs/VfsTextEditor.jsx | 25 ++++++++++--- src/components/vfs/vfsUtils.js | 22 ++++++++++- src/components/warehouse/ArtikelLager.jsx | 3 ++ src/routes/artikel/lager.jsx | 17 ++++++--- src/routes/auftrag/versandarten.jsx | 17 ++++++--- src/routes/kunde/gruppen.jsx | 17 ++++++--- 20 files changed, 300 insertions(+), 82 deletions(-) diff --git a/src/components/carrier/CarrierItem.jsx b/src/components/carrier/CarrierItem.jsx index e7af04f..bf9c4fc 100644 --- a/src/components/carrier/CarrierItem.jsx +++ b/src/components/carrier/CarrierItem.jsx @@ -40,6 +40,7 @@ class CarrierItem extends PureComponent { }; handleBlur = () => { + if (this.props.disabled) return; const item = this.props.carrier ?? this.props.item; if (!item) return; const draft = this.state.draft.trim(); @@ -63,6 +64,7 @@ class CarrierItem extends PureComponent { } handleToggleHasPackages = (e) => { + if (this.props.disabled) return; const item = this.props.carrier ?? this.props.item; if (!item) return; const nextVal = e.target.checked; @@ -70,6 +72,7 @@ class CarrierItem extends PureComponent { }; handleToggleIsReverse = (e) => { + if (this.props.disabled) return; const item = this.props.carrier ?? this.props.item; if (!item) return; const nextVal = e.target.checked; @@ -77,6 +80,7 @@ class CarrierItem extends PureComponent { }; handleKeyDown = (event) => { + if (this.props.disabled) return; const item = this.props.carrier ?? this.props.item; if (event.key === 'Enter') { event.preventDefault(); @@ -90,6 +94,7 @@ class CarrierItem extends PureComponent { }; handleDelete = () => { + if (this.props.disabled) return; const item = this.props.carrier ?? this.props.item; if (!item) return; if (!window.confirm(`Versandart „${item.name}“ wirklich löschen?`)) return; diff --git a/src/components/kunde/KundeGruppe.jsx b/src/components/kunde/KundeGruppe.jsx index 98b54e7..5c3f24a 100644 --- a/src/components/kunde/KundeGruppe.jsx +++ b/src/components/kunde/KundeGruppe.jsx @@ -37,6 +37,7 @@ class KundeGruppe extends PureComponent { }; handleBlur = () => { + if (this.props.disabled) return; const item = this.props.group ?? this.props.item; if (!item) return; const editable = item.editable !== false; @@ -79,6 +80,7 @@ class KundeGruppe extends PureComponent { }; handleDelete = () => { + if (this.props.disabled) return; const item = this.props.group ?? this.props.item; if (!item) return; const editable = item.editable !== false; diff --git a/src/components/settings/BackupSettingsPanel.jsx b/src/components/settings/BackupSettingsPanel.jsx index 9b704a3..278aad1 100644 --- a/src/components/settings/BackupSettingsPanel.jsx +++ b/src/components/settings/BackupSettingsPanel.jsx @@ -44,6 +44,8 @@ import WidgetsOutlinedIcon from '@mui/icons-material/WidgetsOutlined'; import { enqueueSnackbar } from 'notistack'; import { vfs } from '../../api'; import backupAutomat from '../../automats/backupAutomat'; +import companyAutomat from '../../automats/companyAutomat'; +import { isProtectedVfsFile } from '../vfs/vfsUtils'; import { notifyError } from '../../utils/notify'; import { BORDER, @@ -93,6 +95,7 @@ export default class BackupSettingsPanel extends Component { super(props); const { status, error, backupStatus, backups, vfsFilesByBackup, vfsLoadingByBackup } = backupAutomat.state; this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', status, error, backupStatus, @@ -115,6 +118,9 @@ export default class BackupSettingsPanel extends Component { } componentDidMount() { + this.unsubCompany = companyAutomat.subscribe(this, (state) => ({ + isAdmin: state.current?.role === 'admin', + })); this.unsub = backupAutomat.subscribe(this, (state) => ({ status: state.status, error: state.error, @@ -130,6 +136,7 @@ export default class BackupSettingsPanel extends Component { } componentWillUnmount() { + this.unsubCompany?.(); this.unsub?.(); } @@ -155,6 +162,7 @@ export default class BackupSettingsPanel extends Component { } handleOpenConfirm(type) { + if (!this.state.isAdmin) return; this.setState({ confirmRestore: type }); } @@ -215,6 +223,7 @@ export default class BackupSettingsPanel extends Component { render() { const { + isAdmin, status, error, backupStatus, @@ -463,6 +472,7 @@ export default class BackupSettingsPanel extends Component { : } onClick={() => this.handleOpenConfirm('skills')} - disabled={restoringSkills} + disabled={!isAdmin || restoringSkills} sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }} > Skills wiederherstellen @@ -607,7 +617,7 @@ export default class BackupSettingsPanel extends Component { variant="contained" startIcon={restoringApplets ? : } onClick={() => this.handleOpenConfirm('applets')} - disabled={restoringApplets} + disabled={!isAdmin || restoringApplets} sx={{ textTransform: 'none', minHeight: 30, fontSize: '0.8rem', fontWeight: 600 }} > Applets wiederherstellen @@ -740,6 +750,7 @@ class BackupTableRow extends Component { // MARK: calls POST /backup/:id/vfs/recover via backupAutomat.actions.recoverBackupVfs from ./api/index.js async handleRecoverFile(e, path) { e?.stopPropagation?.(); + if (!this.props.isAdmin && isProtectedVfsFile(path)) return; if (this.state.recoveringPath) return; this.setState({ recoveringPath: path }); @@ -991,7 +1002,7 @@ class BackupTableRow extends Component { color="primary" startIcon={isRecovering ? : } onClick={(e) => this.handleRecoverFile(e, f.name)} - disabled={Boolean(recoveringPath)} + disabled={Boolean(recoveringPath) || (!this.props.isAdmin && isProtectedVfsFile(f.name))} sx={{ textTransform: 'none', py: 0.25, px: 1, minHeight: 26, fontSize: '0.75rem' }} > {isRecovering ? 'Wiederherstellen...' : 'Wiederherstellen'} diff --git a/src/components/settings/CompanySettingsPanel.jsx b/src/components/settings/CompanySettingsPanel.jsx index fb6732f..c4ad158 100644 --- a/src/components/settings/CompanySettingsPanel.jsx +++ b/src/components/settings/CompanySettingsPanel.jsx @@ -23,6 +23,7 @@ export default class CompanySettingsPanel extends Component { super(props); const current = companyAutomat.state.current; this.state = { + isAdmin: current?.role === 'admin', status: current ? 'ready' : 'loading', busy: false, error: null, @@ -42,6 +43,9 @@ export default class CompanySettingsPanel extends Component { if (state.current && this.state.status === 'loading') { this.syncFormFromCompany(state.current); } + return { + isAdmin: state.current?.role === 'admin', + }; }); if (companyAutomat.state.current) { this.syncFormFromCompany(companyAutomat.state.current); @@ -86,6 +90,7 @@ export default class CompanySettingsPanel extends Component { async handleSubmit(event) { event?.preventDefault?.(); + if (!this.state.isAdmin) return; const { name, iso639, taxrate, reducedtaxrate } = this.state; const parsedTaxrate = parseTaxRate(taxrate); const parsedReducedtaxrate = parseTaxRate(reducedtaxrate); @@ -123,7 +128,7 @@ export default class CompanySettingsPanel extends Component { } render() { - const { status, busy, error, name, iso639, taxrate, reducedtaxrate } = this.state; + const { isAdmin, status, busy, error, name, iso639, taxrate, reducedtaxrate } = this.state; if (status === 'loading' && !name) { return ( @@ -146,6 +151,7 @@ export default class CompanySettingsPanel extends Component { taxrate.trim() !== this._initial.taxrate.trim() || reducedtaxrate.trim() !== this._initial.reducedtaxrate.trim()); const canSave = + isAdmin && isDirty && name.trim() !== '' && ISO639_OPTIONS.includes(iso639) && @@ -175,7 +181,7 @@ export default class CompanySettingsPanel extends Component { size="small" label="Name" value={name} - disabled={busy} + disabled={!isAdmin || busy} onChange={(nextName) => this.setState({ name: nextName })} autoComplete="off" autoFocus @@ -188,7 +194,7 @@ export default class CompanySettingsPanel extends Component { required size="small" value={iso639} - disabled={busy} + disabled={!isAdmin || busy} onChange={(_event, value) => { if (value) this.setState({ iso639: value }); }} @@ -206,7 +212,7 @@ export default class CompanySettingsPanel extends Component { type="number" label="Normalsatz" value={taxrate} - disabled={busy} + disabled={!isAdmin || busy} onChange={(e) => this.setState({ taxrate: e.target.value })} error={taxrate !== '' && parsedTaxrate == null} slotProps={{ @@ -223,7 +229,7 @@ export default class CompanySettingsPanel extends Component { type="number" label="Ermäßigter Satz" value={reducedtaxrate} - disabled={busy} + disabled={!isAdmin || busy} onChange={(e) => this.setState({ reducedtaxrate: e.target.value })} error={reducedtaxrate !== '' && parsedReducedtaxrate == null} slotProps={{ @@ -240,7 +246,7 @@ export default class CompanySettingsPanel extends Component { type="submit" variant="contained" color={canSave ? 'success' : 'inherit'} - disabled={busy || !canSave} + disabled={!isAdmin || busy || !canSave} startIcon={busy ? : null} sx={ canSave diff --git a/src/components/settings/ImportExportSettingsPanel.jsx b/src/components/settings/ImportExportSettingsPanel.jsx index a5b3e3a..90d3401 100644 --- a/src/components/settings/ImportExportSettingsPanel.jsx +++ b/src/components/settings/ImportExportSettingsPanel.jsx @@ -50,6 +50,7 @@ export default class ImportExportSettingsPanel extends Component { constructor(props) { super(props); this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', // Export state downloading: false, downloadProgress: null, // null = unknown, number 0-100 @@ -84,7 +85,14 @@ export default class ImportExportSettingsPanel extends Component { this.handleCancelUpload = this.handleCancelUpload.bind(this); } + componentDidMount() { + this.unsub = companyAutomat.subscribe(this, (state) => ({ + isAdmin: state.current?.role === 'admin', + })); + } + componentWillUnmount() { + this.unsub?.(); if (this.uploadAbortController) { this.uploadAbortController.abort(); } @@ -147,6 +155,7 @@ export default class ImportExportSettingsPanel extends Component { } handleFileChange(event) { + if (!this.state.isAdmin) return; const file = event.target.files?.[0]; if (file) { if (!isZipFile(file)) { @@ -169,6 +178,7 @@ export default class ImportExportSettingsPanel extends Component { } async handleStartUpload() { + if (!this.state.isAdmin) return; const { selectedFile } = this.state; if (!selectedFile) return; @@ -282,6 +292,7 @@ export default class ImportExportSettingsPanel extends Component { render() { const { + isAdmin, downloading, downloadProgress, downloadStatus, @@ -583,7 +594,7 @@ export default class ImportExportSettingsPanel extends Component { accept=".zip,application/zip,application/x-zip-compressed" style={{ display: 'none' }} onChange={this.handleFileChange} - disabled={uploading} + disabled={!isAdmin || uploading} /> {!selectedFile ? ( @@ -592,7 +603,7 @@ export default class ImportExportSettingsPanel extends Component { variant="outlined" startIcon={} onClick={() => this.fileInputRef.current?.click()} - disabled={uploading} + disabled={!isAdmin || uploading} > ZIP-Datei auswählen @@ -625,6 +636,7 @@ export default class ImportExportSettingsPanel extends Component { {!uploading && ( { this.setState({ selectedFile: null }); if (this.fileInputRef.current) this.fileInputRef.current.value = ''; @@ -760,7 +772,7 @@ export default class ImportExportSettingsPanel extends Component { ) } - disabled={uploading || !selectedFile} + disabled={!isAdmin || uploading || !selectedFile} onClick={this.handleStartUpload} > {uploading ? 'Import läuft…' : 'Upload & Import starten'} diff --git a/src/components/settings/NumberSettingsPanel.jsx b/src/components/settings/NumberSettingsPanel.jsx index b63d14e..e6880b2 100644 --- a/src/components/settings/NumberSettingsPanel.jsx +++ b/src/components/settings/NumberSettingsPanel.jsx @@ -59,6 +59,7 @@ export default class NumberSettingsPanel extends PureComponent { : []; const numbersStatus = companyAutomat.state.numbersStatus; this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', status: numbersStatus === 'ready' ? 'ready' @@ -81,6 +82,7 @@ export default class NumberSettingsPanel extends PureComponent { this.unsub = companyAutomat.subscribe(this, (state) => { const numbers = Array.isArray(state.numbers) ? state.numbers : []; return { + isAdmin: state.current?.role === 'admin', status: state.numbersStatus === 'ready' ? 'ready' @@ -121,6 +123,7 @@ export default class NumberSettingsPanel extends PureComponent { } async handleSave(id) { + if (!this.state.isAdmin) return; const draft = this.state.drafts[id]; if (!draft || draft.busy) return; const row = this.state.numbers.find((r) => r.id === id); @@ -211,7 +214,7 @@ export default class NumberSettingsPanel extends PureComponent { } render() { - const { status, error, numbers, drafts } = this.state; + const { isAdmin, status, error, numbers, drafts } = this.state; if (status === 'loading') { return ( @@ -249,7 +252,7 @@ export default class NumberSettingsPanel extends PureComponent { this.handleChange(row.id, 'prefix', e.target.value)} sx={{ input: { fontFamily: 'monospace' } }} /> @@ -259,7 +262,7 @@ export default class NumberSettingsPanel extends PureComponent { size="small" type="number" value={draft.padding} - disabled={draft.busy} + disabled={!isAdmin || draft.busy} slotProps={{ htmlInput: { min: 1, step: 1 } }} onChange={(e) => this.handleChange(row.id, 'padding', e.target.value)} sx={{ input: { fontFamily: 'monospace' } }} @@ -270,7 +273,7 @@ export default class NumberSettingsPanel extends PureComponent { size="small" type="number" value={draft.next} - disabled={draft.busy} + disabled={!isAdmin || draft.busy} slotProps={{ htmlInput: { min: 0, step: 1 } }} onChange={(e) => this.handleChange(row.id, 'next', e.target.value)} sx={{ input: { fontFamily: 'monospace' } }} @@ -295,7 +298,7 @@ export default class NumberSettingsPanel extends PureComponent { size="small" aria-label="Speichern" color="primary" - disabled={draft.busy} + disabled={!isAdmin || draft.busy} onClick={() => void this.handleSave(row.id)} > diff --git a/src/components/settings/PosTerminalSettingsPanel.jsx b/src/components/settings/PosTerminalSettingsPanel.jsx index ad5782a..3b3c0a8 100644 --- a/src/components/settings/PosTerminalSettingsPanel.jsx +++ b/src/components/settings/PosTerminalSettingsPanel.jsx @@ -64,6 +64,7 @@ export default class PosTerminalSettingsPanel extends Component { super(props); const terminals = companyAutomat.state.terminals ?? []; this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', status: companyAutomat.state.terminalsStatus === 'succeeded' ? 'ready' : 'loading', error: null, terminals, @@ -93,6 +94,7 @@ export default class PosTerminalSettingsPanel extends Component { componentDidMount() { this.unsub = companyAutomat.subscribe(this, (state) => ({ + isAdmin: state.current?.role === 'admin', terminals: Array.isArray(state.terminals) ? state.terminals : [], status: state.terminalsStatus === 'succeeded' ? 'ready' : state.terminalsStatus === 'failed' ? 'failed' : this.state.status, })); @@ -133,6 +135,7 @@ export default class PosTerminalSettingsPanel extends Component { } async handleCreateTerminal() { + if (!this.state.isAdmin) return; const { newTerminalName, enableAllForNew, terminals } = this.state; const trimmed = newTerminalName.trim(); if (!trimmed) return; @@ -294,6 +297,7 @@ export default class PosTerminalSettingsPanel extends Component { render() { const { + isAdmin, status, error, terminals, @@ -362,7 +366,7 @@ export default class PosTerminalSettingsPanel extends Component { placeholder="Neues Terminal..." value={newTerminalName} onChange={(e) => this.setState({ newTerminalName: e.target.value })} - disabled={creatingTerminal} + disabled={!isAdmin || creatingTerminal} autoComplete="off" slotProps={{ htmlInput: { @@ -387,7 +391,7 @@ export default class PosTerminalSettingsPanel extends Component { size="small" checked={enableAllForNew} onChange={(e) => this.setState({ enableAllForNew: e.target.checked })} - disabled={creatingTerminal} + disabled={!isAdmin || creatingTerminal} /> @@ -396,7 +400,7 @@ export default class PosTerminalSettingsPanel extends Component { size="small" startIcon={creatingTerminal ? : } onClick={this.handleCreateTerminal} - disabled={creatingTerminal || !newTerminalName.trim()} + disabled={!isAdmin || creatingTerminal || !newTerminalName.trim()} > Hinzufügen @@ -492,7 +496,7 @@ export default class PosTerminalSettingsPanel extends Component { void this.handleToggleEnabled(terminal, e.target.checked)} color="success" /> @@ -516,7 +520,7 @@ export default class PosTerminalSettingsPanel extends Component { size="small" aria-label={`Terminal ${terminal.name} löschen`} onClick={() => void this.handleDeleteTerminal(terminal)} - disabled={isDeleting || creatingTerminal || isPatching} + disabled={!isAdmin || isDeleting || creatingTerminal || isPatching} color="error" > {isDeleting ? ( @@ -546,13 +550,14 @@ export default class PosTerminalSettingsPanel extends Component { } label={this.getCategoryLabel(terminal)} - onClick={() => this.setState({ pickerTerminal: terminal })} + onClick={isAdmin ? () => this.setState({ pickerTerminal: terminal }) : undefined} onDelete={ - terminal.rootCategoryId != null + isAdmin && terminal.rootCategoryId != null ? () => void this.handleSetRootCategory(terminal, null) : undefined } - clickable + clickable={isAdmin} + disabled={!isAdmin || isPatching || isDeleting} variant={terminal.rootCategoryId != null ? 'filled' : 'outlined'} color={terminal.rootCategoryId != null ? 'primary' : 'default'} size="small" @@ -560,6 +565,7 @@ export default class PosTerminalSettingsPanel extends Component { @@ -716,7 +720,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { this.handleStartRename(agent)} - disabled={isPatching || isDeleting} + disabled={!isAdmin || isPatching || isDeleting} sx={{ p: 0.5, color: 'text.secondary' }} > @@ -764,7 +768,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { void this.handleToggleAgentEnabled(agent, e.target.checked)} color="success" /> @@ -788,7 +792,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { size="small" aria-label={`Drucker-Agent ${agent.name} löschen`} onClick={() => void this.handleDeleteAgent(agent)} - disabled={isDeleting || creatingAgent || isPatching} + disabled={!isAdmin || isDeleting || creatingAgent || isPatching} color="error" > @@ -904,7 +908,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { void this.handleMoveAgent(agent, 'up')} sx={{ p: 0.25 }} > @@ -914,7 +918,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { void this.handleMoveAgent(agent, 'down')} sx={{ p: 0.25 }} > @@ -924,7 +928,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { this.handleSortDraftChange(agent.id, e.target.value)} onBlur={() => void this.handleSortSubmit(agent)} @@ -1078,6 +1082,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { void this.handleTogglePrinterEnabled(p, e.target.checked) } @@ -1099,7 +1104,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { void this.handleMovePrinter(p, 'up')} sx={{ p: 0.25 }} > @@ -1110,7 +1115,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { void this.handleMovePrinter(p, 'down')} sx={{ p: 0.25 }} > @@ -1124,7 +1129,7 @@ export default class PrinterAgentSettingsPanel extends PureComponent { void this.handleDeletePrinter(p)} sx={{ p: 0.5 }} > diff --git a/src/components/settings/PrinterSettingsPanel.jsx b/src/components/settings/PrinterSettingsPanel.jsx index 48870e3..dcaa0fb 100644 --- a/src/components/settings/PrinterSettingsPanel.jsx +++ b/src/components/settings/PrinterSettingsPanel.jsx @@ -26,6 +26,7 @@ import EditOutlinedIcon from '@mui/icons-material/EditOutlined'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../BackendError'; import { printer as printerApi } from '../../api'; +import companyAutomat from '../../automats/companyAutomat'; import { requireArray, requireObject } from '../../utils/apiShape'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; @@ -189,6 +190,7 @@ export default class PrinterSettingsPanel extends Component { constructor(props) { super(props); this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', status: 'loading', error: null, printers: [], @@ -208,6 +210,9 @@ export default class PrinterSettingsPanel extends Component { } componentDidMount() { + this.unsub = companyAutomat.subscribe(this, (state) => ({ + isAdmin: state.current?.role === 'admin', + })); void this.load(); } @@ -326,10 +331,12 @@ export default class PrinterSettingsPanel extends Component { componentWillUnmount() { this._unmounted = true; + this.unsub?.(); } render() { const { + isAdmin, status, error, printers, draggingId, dragOverId, addOpen, editPrinter, @@ -354,6 +361,7 @@ export default class PrinterSettingsPanel extends Component { size="small" variant="outlined" startIcon={} + disabled={!isAdmin} onClick={this.handleAdd} > Drucker hinzufügen @@ -386,17 +394,17 @@ export default class PrinterSettingsPanel extends Component { this.handleDragStart(printer.id)} - onDragOver={(e) => this.handleDragOver(e, printer.id)} - onDragLeave={() => this.handleDragLeave()} - onDrop={() => this.handleDrop(printer.id)} - onDragEnd={() => this.handleDragEnd()} + draggable={isAdmin} + onDragStart={isAdmin ? () => this.handleDragStart(printer.id) : undefined} + onDragOver={isAdmin ? (e) => this.handleDragOver(e, printer.id) : undefined} + onDragLeave={isAdmin ? () => this.handleDragLeave() : undefined} + onDrop={isAdmin ? () => this.handleDrop(printer.id) : undefined} + onDragEnd={isAdmin ? () => this.handleDragEnd() : undefined} sx={{ opacity: isDragging ? 0.4 : 1, outline: isDragOver ? '2px solid' : 'none', outlineColor: 'primary.main', - cursor: 'grab', + cursor: isAdmin ? 'grab' : 'default', transition: 'opacity 0.15s', }} > @@ -426,7 +434,7 @@ export default class PrinterSettingsPanel extends Component { this.setState({ editPrinter: printer })} > @@ -435,7 +443,7 @@ export default class PrinterSettingsPanel extends Component { size="small" aria-label="Löschen" color="error" - disabled={isDeleting} + disabled={!isAdmin || isDeleting} onClick={() => void this.handleDelete(printer.id)} > {isDeleting ? ( diff --git a/src/components/settings/SettingsKeysPanel.jsx b/src/components/settings/SettingsKeysPanel.jsx index c306c06..4cf0980 100644 --- a/src/components/settings/SettingsKeysPanel.jsx +++ b/src/components/settings/SettingsKeysPanel.jsx @@ -16,6 +16,7 @@ import SaveOutlinedIcon from '@mui/icons-material/SaveOutlined'; import { enqueueSnackbar } from 'notistack'; import BackendError from '../BackendError'; import { setting as settingApi } from '../../api'; +import companyAutomat from '../../automats/companyAutomat'; import { requireArray, requireDefined, requireObject } from '../../utils/apiShape'; import { backendErrorFrom } from '../../utils/backendError'; import { notifyError } from '../../utils/notify'; @@ -68,6 +69,7 @@ export default class SettingsKeysPanel extends Component { constructor(props) { super(props); this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', status: 'loading', error: null, settings: [], @@ -79,6 +81,9 @@ export default class SettingsKeysPanel extends Component { } componentDidMount() { + this.unsub = companyAutomat.subscribe(this, (state) => ({ + isAdmin: state.current?.role === 'admin', + })); void this.load(); } @@ -124,6 +129,7 @@ export default class SettingsKeysPanel extends Component { } async handleSave(key) { + if (!this.state.isAdmin) return; const draft = this.state.drafts[key]; if (!draft || draft.busy) return; const row = this.state.settings.find((s) => s.key === key); @@ -172,10 +178,11 @@ export default class SettingsKeysPanel extends Component { componentWillUnmount() { this._unmounted = true; + this.unsub?.(); } render() { - const { status, error, settings, drafts } = this.state; + const { isAdmin, status, error, settings, drafts } = this.state; if (status === 'loading') { return ( @@ -224,7 +231,7 @@ export default class SettingsKeysPanel extends Component { size="small" fullWidth value={draft.value !== undefined && draft.value !== null ? draft.value : (DEFAULT_SETTINGS[row.key] ?? '')} - disabled={draft.busy} + disabled={!isAdmin || draft.busy} onChange={(e) => this.handleChange(row.key, 'value', e.target.value)} slotProps={{ htmlInput: { spellCheck: false } }} > @@ -247,7 +254,7 @@ export default class SettingsKeysPanel extends Component { maxRows={isMultiline ? 6 : undefined} placeholder={DEFAULT_SETTINGS[row.key] ?? undefined} value={draft.value} - disabled={draft.busy} + disabled={!isAdmin || draft.busy} onChange={(e) => this.handleChange(row.key, 'value', e.target.value)} onKeyDown={(e) => { if (!isMultiline && e.key === 'Enter') { @@ -279,7 +286,7 @@ export default class SettingsKeysPanel extends Component { size="small" aria-label="Speichern" color="primary" - disabled={draft.busy} + disabled={!isAdmin || draft.busy} onClick={() => void this.handleSave(row.key)} > diff --git a/src/components/vfs/VfsEditorPanel.jsx b/src/components/vfs/VfsEditorPanel.jsx index 81a0e3a..c35d3a5 100644 --- a/src/components/vfs/VfsEditorPanel.jsx +++ b/src/components/vfs/VfsEditorPanel.jsx @@ -5,19 +5,21 @@ import IconButton from '@mui/material/IconButton'; import FolderOpenRoundedIcon from '@mui/icons-material/FolderOpenRounded'; import CloseRoundedIcon from '@mui/icons-material/CloseRounded'; +import companyAutomat from '../../automats/companyAutomat'; import vfsAutomat from '../../automats/vfsAutomat'; import VfsFileExplorer from './VfsFileExplorer'; import VfsTextEditor from './VfsTextEditor'; import VfsFilePreview from './VfsFilePreview'; import VfsAppletRunner from './VfsAppletRunner'; import VfsNewFileDialog from './VfsNewFileDialog'; -import { isAppletFile } from './vfsUtils'; +import { isAppletFile, isProtectedVfsFile } from './vfsUtils'; export default class VfsEditorPanel extends PureComponent { constructor(props) { super(props); const s = vfsAutomat.state; this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', files: s.files, loadingFiles: s.loadingFiles, loadError: s.loadError, @@ -39,6 +41,9 @@ export default class VfsEditorPanel extends PureComponent { } componentDidMount() { + this.unsubCompany = companyAutomat.subscribe(this, (state) => ({ + isAdmin: state.current?.role === 'admin', + })); this.unsub = vfsAutomat.subscribe(this, (state) => ({ files: state.files, loadingFiles: state.loadingFiles, @@ -72,6 +77,7 @@ export default class VfsEditorPanel extends PureComponent { } componentWillUnmount() { + this.unsubCompany?.(); this.unsub?.(); } @@ -108,6 +114,7 @@ export default class VfsEditorPanel extends PureComponent { render() { const { + isAdmin, files, loadingFiles, selectedFileName, @@ -124,6 +131,7 @@ export default class VfsEditorPanel extends PureComponent { } = this.state; const isApplet = Boolean(selectedFileName && isAppletFile(selectedFileName)); + const isProtectedFile = Boolean(selectedFileName && isProtectedVfsFile(selectedFileName)); const showAppletRunner = isApplet && selectedFile?.isText && appletView === 'run'; return ( @@ -161,6 +169,8 @@ export default class VfsEditorPanel extends PureComponent { isSaving={isSaving} saveError={saveError} loadingContent={loadingContent} + isAdmin={isAdmin} + readOnly={!isAdmin && isProtectedFile} onChange={(c) => vfsAutomat.actions.updateContent(c)} onSave={() => vfsAutomat.actions.saveCurrentFile()} onClose={() => vfsAutomat.actions.selectFile(null)} @@ -216,6 +226,7 @@ export default class VfsEditorPanel extends PureComponent { vfsAutomat.actions.selectFile(name)} @@ -238,6 +249,7 @@ export default class VfsEditorPanel extends PureComponent { vfsAutomat.actions.setNewFileDialogOpen(false)} onCreate={(name) => vfsAutomat.actions.createFile(name)} isCreating={isSaving} diff --git a/src/components/vfs/VfsFileExplorer.jsx b/src/components/vfs/VfsFileExplorer.jsx index 5225519..8597be8 100644 --- a/src/components/vfs/VfsFileExplorer.jsx +++ b/src/components/vfs/VfsFileExplorer.jsx @@ -27,11 +27,25 @@ import CreateNewFolderRoundedIcon from '@mui/icons-material/CreateNewFolderRound import DriveFileRenameOutlineRoundedIcon from '@mui/icons-material/DriveFileRenameOutlineRounded'; import PlayArrowRoundedIcon from '@mui/icons-material/PlayArrowRounded'; +import companyAutomat from '../../automats/companyAutomat'; import VfsFileIcon from './VfsFileIcon'; -import { formatVfsSize, formatVfsDate, isAppletFile } from './vfsUtils'; +import { formatVfsSize, formatVfsDate, isAppletFile, isProtectedVfsFile } from './vfsUtils'; import VfsNewFolderDialog from './VfsNewFolderDialog'; import VfsRenameDialog from './VfsRenameDialog'; +/** + * Returns true if the given folder path contains any admin-protected VFS files + * (`.skill` or `.applet.js`, case-insensitive). + */ +function folderContainsProtectedFiles(folderPath, files) { + if (!folderPath || !Array.isArray(files)) return false; + const prefix = `${String(folderPath).replace(/^\/+/, '').replace(/\/+$/, '')}/`; + return files.some((f) => { + const clean = String(f?.name || '').replace(/^\/+/, ''); + return clean.startsWith(prefix) && isProtectedVfsFile(clean); + }); +} + /** * Returns all ancestor directory paths for a file or folder path. * e.g. "docs/api/v1/spec.json" -> ["docs", "docs/api", "docs/api/v1"] @@ -126,6 +140,7 @@ export default class VfsFileExplorer extends PureComponent { } } this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', menuAnchor: null, menuFile: null, folderMenuAnchor: null, @@ -143,6 +158,16 @@ export default class VfsFileExplorer extends PureComponent { this.fileInputRef = React.createRef(); } + componentDidMount() { + this.unsubCompany = companyAutomat.subscribe(this, (state) => ({ + isAdmin: state.current?.role === 'admin', + })); + } + + componentWillUnmount() { + this.unsubCompany?.(); + } + componentDidUpdate(prevProps) { if (this.props.selectedFileName && this.props.selectedFileName !== prevProps.selectedFileName) { const parents = getParentFolderPaths(this.props.selectedFileName); @@ -632,7 +657,12 @@ export default class VfsFileExplorer extends PureComponent { isRenaming, isDraggingOver, } = this.state; + const isAdmin = this.props.isAdmin ?? this.state.isAdmin; const isText = menuFile?.isText; + const isMenuFileProtected = Boolean(menuFile && isProtectedVfsFile(menuFile.name)); + const isFolderProtected = Boolean( + folderMenuNode && folderContainsProtectedFiles(folderMenuNode.path, this.props.files) + ); return ( { const file = menuFile; this.handleCloseMenu(); @@ -860,6 +891,7 @@ export default class VfsFileExplorer extends PureComponent { )} { if (menuFile && window.confirm(`Möchtest du "${menuFile.name}" wirklich aus dem VFS löschen?`)) { this.props.onDelete(menuFile.name); @@ -915,6 +947,7 @@ export default class VfsFileExplorer extends PureComponent { { const folder = folderMenuNode; this.handleCloseFolderMenu(); @@ -934,6 +967,7 @@ export default class VfsFileExplorer extends PureComponent { { const folder = folderMenuNode; this.handleCloseFolderMenu(); @@ -963,6 +997,7 @@ export default class VfsFileExplorer extends PureComponent { this.setState({ renameDialogOpen: false, renameItem: null })} onRename={this.handleRenameItem} isRenaming={isRenaming} diff --git a/src/components/vfs/VfsNewFileDialog.jsx b/src/components/vfs/VfsNewFileDialog.jsx index 9b74613..b676c59 100644 --- a/src/components/vfs/VfsNewFileDialog.jsx +++ b/src/components/vfs/VfsNewFileDialog.jsx @@ -7,17 +7,30 @@ import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; import NoteAddRoundedIcon from '@mui/icons-material/NoteAddRounded'; import SingleLineTextEdit from 'dashgui/SingleLineTextEdit'; +import companyAutomat from '../../automats/companyAutomat'; +import { isProtectedVfsFile } from './vfsUtils'; export default class VfsNewFileDialog extends PureComponent { constructor(props) { super(props); const folder = props.defaultFolder ? `${String(props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : ''; this.state = { + isAdmin: companyAutomat.state.current?.role === 'admin', filename: folder, error: '', }; } + componentDidMount() { + this.unsub = companyAutomat.subscribe(this, (state) => ({ + isAdmin: state.current?.role === 'admin', + })); + } + + componentWillUnmount() { + this.unsub?.(); + } + componentDidUpdate(prevProps) { if (!prevProps.open && this.props.open) { const folder = this.props.defaultFolder ? `${String(this.props.defaultFolder).replace(/^\/+/, '').replace(/\/+$/, '')}/` : ''; @@ -27,6 +40,7 @@ export default class VfsNewFileDialog extends PureComponent { handleSubmit = (e) => { e?.preventDefault(); + const isAdmin = this.props.isAdmin ?? this.state.isAdmin; const clean = this.state.filename.trim().replace(/^[/\\]+/, ''); if (!clean) { this.setState({ error: 'Bitte gib einen Dateinamen ein.' }); @@ -36,12 +50,21 @@ export default class VfsNewFileDialog extends PureComponent { this.setState({ error: 'Ungültige Zeichen im Dateinamen.' }); return; } + if (!isAdmin && isProtectedVfsFile(clean)) { + this.setState({ error: 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien erstellen.' }); + return; + } this.props.onCreate(clean); }; render() { const { open, onClose, isCreating } = this.props; const { filename, error } = this.state; + const isAdmin = this.props.isAdmin ?? this.state.isAdmin; + const clean = filename.trim().replace(/^[/\\]+/, ''); + const isProtected = Boolean(!isAdmin && clean && isProtectedVfsFile(clean)); + const displayError = + error || (isProtected ? 'Nur Administratoren dürfen .SKILL- und .applet.js-Dateien erstellen.' : ''); return ( this.setState({ filename: nextVal, error: '' })} - error={Boolean(error)} - helperText={error} + error={Boolean(displayError)} + helperText={displayError} disabled={isCreating} /> @@ -83,7 +106,7 @@ export default class VfsNewFileDialog extends PureComponent {