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 (