fix: replace inline save banners with toast notifications
This commit is contained in:
@@ -23,9 +23,9 @@ function DiscoveryPanel({ spaceId }: { spaceId: string }) {
|
|||||||
(space?.visibility as SpaceVisibility) ?? 'private'
|
(space?.visibility as SpaceVisibility) ?? 'private'
|
||||||
);
|
);
|
||||||
const [description, setDescription] = useState(space?.description ?? '');
|
const [description, setDescription] = useState(space?.description ?? '');
|
||||||
|
const addToast = useUIStore((s) => s.addToast);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState('');
|
const [saveError, setSaveError] = useState('');
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (space) {
|
if (space) {
|
||||||
@@ -43,11 +43,9 @@ function DiscoveryPanel({ spaceId }: { spaceId: string }) {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
setSaveSuccess(false);
|
|
||||||
try {
|
try {
|
||||||
await api.spaces.update(spaceId, { visibility, description: description.trim() });
|
await api.spaces.update(spaceId, { visibility, description: description.trim() });
|
||||||
setSaveSuccess(true);
|
addToast('Settings saved', 'success', 2000);
|
||||||
setTimeout(() => setSaveSuccess(false), 2000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -126,10 +124,6 @@ function DiscoveryPanel({ spaceId }: { spaceId: string }) {
|
|||||||
{saveError && (
|
{saveError && (
|
||||||
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
||||||
)}
|
)}
|
||||||
{saveSuccess && (
|
|
||||||
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Pending Join Requests — only shown when visibility is 'request' */}
|
{/* Pending Join Requests — only shown when visibility is 'request' */}
|
||||||
{(visibility === 'request' || (space.visibility as SpaceVisibility) === 'request') && (
|
{(visibility === 'request' || (space.visibility as SpaceVisibility) === 'request') && (
|
||||||
<JoinRequestsSection spaceId={spaceId} />
|
<JoinRequestsSection spaceId={spaceId} />
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useSettingsStore } from '../../../stores/settingsStore';
|
import { useSettingsStore } from '../../../stores/settingsStore';
|
||||||
|
import { useUIStore } from '../../../stores/uiStore';
|
||||||
import { Toggle } from '../../ui/Toggle';
|
import { Toggle } from '../../ui/Toggle';
|
||||||
import type { InstanceAdminSettings } from '@backspace/shared';
|
import type { InstanceAdminSettings } from '@backspace/shared';
|
||||||
|
|
||||||
@@ -7,10 +8,11 @@ export function GeneralPanel() {
|
|||||||
const instanceSettings = useSettingsStore((s) => s.instanceSettings);
|
const instanceSettings = useSettingsStore((s) => s.instanceSettings);
|
||||||
const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings);
|
const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings);
|
||||||
|
|
||||||
|
const addToast = useUIStore((s) => s.addToast);
|
||||||
|
|
||||||
const [draft, setDraft] = useState<InstanceAdminSettings | null>(null);
|
const [draft, setDraft] = useState<InstanceAdminSettings | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState('');
|
const [saveError, setSaveError] = useState('');
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
||||||
const [gifKeyDirty, setGifKeyDirty] = useState(false);
|
const [gifKeyDirty, setGifKeyDirty] = useState(false);
|
||||||
const [gifKeyDraft, setGifKeyDraft] = useState('');
|
const [gifKeyDraft, setGifKeyDraft] = useState('');
|
||||||
|
|
||||||
@@ -34,7 +36,6 @@ export function GeneralPanel() {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
setSaveSuccess(false);
|
|
||||||
try {
|
try {
|
||||||
const payload: Partial<InstanceAdminSettings> = {
|
const payload: Partial<InstanceAdminSettings> = {
|
||||||
instanceName: draft!.instanceName,
|
instanceName: draft!.instanceName,
|
||||||
@@ -47,8 +48,7 @@ export function GeneralPanel() {
|
|||||||
await updateInstanceSettings(payload);
|
await updateInstanceSettings(payload);
|
||||||
setGifKeyDirty(false);
|
setGifKeyDirty(false);
|
||||||
setGifKeyDraft('');
|
setGifKeyDraft('');
|
||||||
setSaveSuccess(true);
|
addToast('Settings saved', 'success', 2000);
|
||||||
setTimeout(() => setSaveSuccess(false), 2000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -151,10 +151,6 @@ export function GeneralPanel() {
|
|||||||
{saveError && (
|
{saveError && (
|
||||||
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
||||||
)}
|
)}
|
||||||
{saveSuccess && (
|
|
||||||
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Save / Reset bar */}
|
{/* Save / Reset bar */}
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<div className="sticky bottom-0 z-10 pointer-events-none">
|
<div className="sticky bottom-0 z-10 pointer-events-none">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useSettingsStore } from '../../../stores/settingsStore';
|
import { useSettingsStore } from '../../../stores/settingsStore';
|
||||||
|
import { useUIStore } from '../../../stores/uiStore';
|
||||||
import type { InstanceStreamingLimits } from '@backspace/shared';
|
import type { InstanceStreamingLimits } from '@backspace/shared';
|
||||||
import {
|
import {
|
||||||
STANDARD_RESOLUTIONS, STANDARD_FRAMERATES,
|
STANDARD_RESOLUTIONS, STANDARD_FRAMERATES,
|
||||||
@@ -19,10 +20,12 @@ export function StreamingPanel() {
|
|||||||
const limits = useSettingsStore((s) => s.streamingLimits);
|
const limits = useSettingsStore((s) => s.streamingLimits);
|
||||||
const updateStreamingLimits = useSettingsStore((s) => s.updateStreamingLimits);
|
const updateStreamingLimits = useSettingsStore((s) => s.updateStreamingLimits);
|
||||||
|
|
||||||
|
const addToast = useUIStore((s) => s.addToast);
|
||||||
|
|
||||||
const [draft, setDraft] = useState<InstanceStreamingLimits | null>(null);
|
const [draft, setDraft] = useState<InstanceStreamingLimits | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState('');
|
const [saveError, setSaveError] = useState('');
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
||||||
|
|
||||||
// Matrix editor state: full grid of kbps values (integers only)
|
// Matrix editor state: full grid of kbps values (integers only)
|
||||||
const [matrixDraft, setMatrixDraft] = useState<Record<string, number>>({});
|
const [matrixDraft, setMatrixDraft] = useState<Record<string, number>>({});
|
||||||
@@ -93,7 +96,6 @@ export function StreamingPanel() {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
setSaveSuccess(false);
|
|
||||||
try {
|
try {
|
||||||
// Compute sparse overrides: only cells that differ from defaults
|
// Compute sparse overrides: only cells that differ from defaults
|
||||||
const overrides: Record<string, number> = {};
|
const overrides: Record<string, number> = {};
|
||||||
@@ -107,8 +109,7 @@ export function StreamingPanel() {
|
|||||||
bitrateMatrixOverrides: Object.keys(overrides).length > 0 ? overrides : null,
|
bitrateMatrixOverrides: Object.keys(overrides).length > 0 ? overrides : null,
|
||||||
};
|
};
|
||||||
await updateStreamingLimits(payload);
|
await updateStreamingLimits(payload);
|
||||||
setSaveSuccess(true);
|
addToast('Settings saved', 'success', 2000);
|
||||||
setTimeout(() => setSaveSuccess(false), 2000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -486,9 +487,6 @@ export function StreamingPanel() {
|
|||||||
{saveError && (
|
{saveError && (
|
||||||
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
||||||
)}
|
)}
|
||||||
{saveSuccess && (
|
|
||||||
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
|
|
||||||
)}
|
|
||||||
{hasChanges && (
|
{hasChanges && (
|
||||||
<div className="sticky bottom-0 z-10 pointer-events-none">
|
<div className="sticky bottom-0 z-10 pointer-events-none">
|
||||||
<div className="flex justify-center pt-3 pb-1">
|
<div className="flex justify-center pt-3 pb-1">
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ export function OverviewPanel({ spaceId }: OverviewPanelProps) {
|
|||||||
|
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saveError, setSaveError] = useState('');
|
const [saveError, setSaveError] = useState('');
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
@@ -174,7 +173,6 @@ export function OverviewPanel({ spaceId }: OverviewPanelProps) {
|
|||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
setSaveSuccess(false);
|
|
||||||
try {
|
try {
|
||||||
const updates: { name?: string; icon?: string; banner?: string; avatarColor?: string } = {};
|
const updates: { name?: string; icon?: string; banner?: string; avatarColor?: string } = {};
|
||||||
if (hasNameChange) updates.name = spaceName.trim();
|
if (hasNameChange) updates.name = spaceName.trim();
|
||||||
@@ -198,8 +196,7 @@ export function OverviewPanel({ spaceId }: OverviewPanelProps) {
|
|||||||
URL.revokeObjectURL(bannerPreview);
|
URL.revokeObjectURL(bannerPreview);
|
||||||
setBannerPreview(null);
|
setBannerPreview(null);
|
||||||
}
|
}
|
||||||
setSaveSuccess(true);
|
addToast('Settings saved', 'success', 2000);
|
||||||
setTimeout(() => setSaveSuccess(false), 2000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
setSaveError(err instanceof Error ? err.message : 'Failed to save');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -449,9 +446,6 @@ export function OverviewPanel({ spaceId }: OverviewPanelProps) {
|
|||||||
{saveError && (
|
{saveError && (
|
||||||
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
||||||
)}
|
)}
|
||||||
{saveSuccess && (
|
|
||||||
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Settings saved</div>
|
|
||||||
)}
|
|
||||||
{/* Danger Zone */}
|
{/* Danger Zone */}
|
||||||
{isOwner && (
|
{isOwner && (
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState, useRef, useEffect } from 'react';
|
import React, { useState, useRef, useEffect } from 'react';
|
||||||
import { useSpaceStore } from '../../../stores/spaceStore';
|
import { useSpaceStore } from '../../../stores/spaceStore';
|
||||||
|
import { useUIStore } from '../../../stores/uiStore';
|
||||||
import { api } from '../../../api/client';
|
import { api } from '../../../api/client';
|
||||||
import { PermissionBits, stringToPermissions, permissionsToString } from '../../../utils/permissions';
|
import { PermissionBits, stringToPermissions, permissionsToString } from '../../../utils/permissions';
|
||||||
import type { Role } from '@backspace/shared';
|
import type { Role } from '@backspace/shared';
|
||||||
@@ -227,10 +228,10 @@ function RoleEditView({ role, spaceId, isNew, onBack, onDeleted, onCopied }: Rol
|
|||||||
const [draftPermissions, setDraftPermissions] = useState<bigint>(
|
const [draftPermissions, setDraftPermissions] = useState<bigint>(
|
||||||
stringToPermissions(role.permissions)
|
stringToPermissions(role.permissions)
|
||||||
);
|
);
|
||||||
|
const addToast = useUIStore((s) => s.addToast);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [deleting, setDeleting] = useState(false);
|
const [deleting, setDeleting] = useState(false);
|
||||||
const [saveError, setSaveError] = useState('');
|
const [saveError, setSaveError] = useState('');
|
||||||
const [saveSuccess, setSaveSuccess] = useState(false);
|
|
||||||
const [confirmDelete, setConfirmDelete] = useState(false);
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
||||||
|
|
||||||
const hasNameChange = !isEveryone && draftName.trim() !== role.name;
|
const hasNameChange = !isEveryone && draftName.trim() !== role.name;
|
||||||
@@ -246,7 +247,6 @@ function RoleEditView({ role, spaceId, isNew, onBack, onDeleted, onCopied }: Rol
|
|||||||
setConfirmDelete(false);
|
setConfirmDelete(false);
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
setSaveError('');
|
setSaveError('');
|
||||||
setSaveSuccess(false);
|
|
||||||
try {
|
try {
|
||||||
const data: { name?: string; color?: string; permissions?: string } = {};
|
const data: { name?: string; color?: string; permissions?: string } = {};
|
||||||
if (hasNameChange) data.name = draftName.trim();
|
if (hasNameChange) data.name = draftName.trim();
|
||||||
@@ -254,8 +254,7 @@ function RoleEditView({ role, spaceId, isNew, onBack, onDeleted, onCopied }: Rol
|
|||||||
if (hasPermChange) data.permissions = permissionsToString(draftPermissions);
|
if (hasPermChange) data.permissions = permissionsToString(draftPermissions);
|
||||||
await api.roles.update(spaceId, role.id, data);
|
await api.roles.update(spaceId, role.id, data);
|
||||||
await loadSpaceDetail(spaceId);
|
await loadSpaceDetail(spaceId);
|
||||||
setSaveSuccess(true);
|
addToast('Role saved', 'success', 2000);
|
||||||
setTimeout(() => setSaveSuccess(false), 2000);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : 'Failed to save role';
|
const msg = err instanceof Error ? err.message : 'Failed to save role';
|
||||||
if (msg.includes('already exists')) {
|
if (msg.includes('already exists')) {
|
||||||
@@ -451,9 +450,6 @@ function RoleEditView({ role, spaceId, isNew, onBack, onDeleted, onCopied }: Rol
|
|||||||
{saveError && (
|
{saveError && (
|
||||||
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
<div className="p-2 bg-accent-rose/10 border border-accent-rose/30 rounded text-txt-danger text-sm">{saveError}</div>
|
||||||
)}
|
)}
|
||||||
{saveSuccess && (
|
|
||||||
<div className="p-2 bg-status-online/10 border border-status-online/30 rounded text-status-online text-sm">Role saved</div>
|
|
||||||
)}
|
|
||||||
<div className="sticky bottom-0 z-10 pointer-events-none">
|
<div className="sticky bottom-0 z-10 pointer-events-none">
|
||||||
<div className="flex justify-center pt-3 pb-1">
|
<div className="flex justify-center pt-3 pb-1">
|
||||||
<div className={`glass-bubble rounded-full px-4 py-2 flex items-center gap-2 pointer-events-auto${
|
<div className={`glass-bubble rounded-full px-4 py-2 flex items-center gap-2 pointer-events-auto${
|
||||||
|
|||||||
Reference in New Issue
Block a user