Skip to content

Commit f814ea4

Browse files
authored
Merge branch 'ONLYOFFICE:master' into master
2 parents 8cf8207 + dabf883 commit f814ea4

90 files changed

Lines changed: 2143 additions & 797 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.lintstagedrc.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
module.exports = {
2-
'*.js': ['eslint', 'prettier'],
2+
'*.js': ['eslint', 'prettier --check'],
33
'*.{json,md,html,css,yml,yaml}': []
44
};

AdminPanel/client/.env.example

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,3 @@
33

44
# Backend URL for API calls
55
REACT_APP_BACKEND_URL=http://localhost:9000
6-
# Docservice Backend URL(only for dev mode)
7-
REACT_APP_DOCSERVICE_URL=http://localhost:8000

AdminPanel/client/package-lock.json

Lines changed: 6 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AdminPanel/client/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "docscloud",
2+
"name": "onlyoffice-adminpanel-client",
33
"version": "1.3.0",
44
"private": true,
55
"scripts": {
@@ -10,6 +10,7 @@
1010
"@reduxjs/toolkit": "^2.8.2",
1111
"@tanstack/react-query": "^5.83.0",
1212
"ajv": "^8.17.1",
13+
"ajv-errors": "^3.0.0",
1314
"ajv-formats": "^3.0.1",
1415
"axios": "1.7.4",
1516
"prop-types": "^15.8.1",

AdminPanel/client/src/App.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ body::-webkit-scrollbar-thumb {
7575
background: #efefef;
7676
}
7777

78+
a {
79+
color: #ff6f3d;
80+
text-decoration: underline;
81+
font-weight: 400;
82+
}
83+
84+
a:hover {
85+
color: #e55a2b;
86+
}
87+
7888
/* Spinner animation */
7989
@keyframes spin {
8090
from {
@@ -84,3 +94,30 @@ body::-webkit-scrollbar-thumb {
8494
transform: rotate(360deg);
8595
}
8696
}
97+
98+
/* Mobile adjustments */
99+
@media (max-width: 767px) {
100+
.mainContent {
101+
padding: 21px;
102+
padding-top: 72px; /* 56px header + 16px content padding */
103+
/* Hide scrollbar UI on mobile while preserving scroll */
104+
-ms-overflow-style: none; /* IE and Edge */
105+
scrollbar-width: none; /* Firefox */
106+
}
107+
108+
.mainContent::-webkit-scrollbar {
109+
width: 0;
110+
height: 0;
111+
display: none; /* Chrome, Safari */
112+
}
113+
114+
.mobileMenuBackdrop {
115+
position: fixed;
116+
top: 0;
117+
left: 0;
118+
right: 0;
119+
bottom: 0;
120+
background: rgba(0, 0, 0, 0.25);
121+
z-index: 1050;
122+
}
123+
}

AdminPanel/client/src/App.js

Lines changed: 34 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,52 @@
11
import {Provider} from 'react-redux';
2+
import {useState} from 'react';
23
import {Routes, Route, Navigate, BrowserRouter} from 'react-router-dom';
34
import './App.css';
45
import {store} from './store';
56
import AuthWrapper from './components/AuthWrapper/AuthWrapper';
67
import ConfigLoader from './components/ConfigLoader/ConfigLoader';
8+
import {useSchemaLoader} from './hooks/useSchemaLoader';
79
import Menu from './components/Menu/Menu';
10+
import MobileHeader from './components/MobileHeader/MobileHeader';
11+
import ScrollToTop from './components/ScrollToTop/ScrollToTop';
812
import {menuItems} from './config/menuItems';
13+
import {getBasename} from './utils/paths';
914

10-
/**
11-
* Simple basename computation from URL path.
12-
* Basename is everything before the last path segment.
13-
* Examples:
14-
* - '/statistics' -> basename ''
15-
* - '/admin/' -> basename '/admin'
16-
* - '/admin/statistics' -> basename '/admin'
17-
* - '/admin/su/statistics' -> basename '/admin/su'
18-
* @returns {string} basename
19-
*/
20-
const getBasename = () => {
21-
const path = window.location.pathname || '/';
22-
if (path === '/') return '';
23-
// Treat '/prefix/' as a directory prefix
24-
if (path.endsWith('/')) return path.slice(0, -1);
25-
// Remove trailing slash (keep root '/') for consistent parsing
26-
const normalized = path;
27-
const lastSlash = normalized.lastIndexOf('/');
28-
// If no parent directory, there is no basename
29-
if (lastSlash <= 0) return '';
30-
return normalized.slice(0, lastSlash);
31-
};
15+
function AppContent() {
16+
useSchemaLoader();
17+
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
18+
19+
return (
20+
<div className='app'>
21+
<AuthWrapper>
22+
<MobileHeader isOpen={isMobileMenuOpen} onMenuToggle={() => setIsMobileMenuOpen(prev => !prev)} />
23+
<div className='appLayout'>
24+
<Menu isOpen={isMobileMenuOpen} onClose={() => setIsMobileMenuOpen(false)} />
25+
{isMobileMenuOpen ? <div className='mobileMenuBackdrop' onClick={() => setIsMobileMenuOpen(false)} aria-hidden='true'></div> : null}
26+
<div className='mainContent'>
27+
<ScrollToTop />
28+
<ConfigLoader>
29+
<Routes>
30+
<Route path='/' element={<Navigate to='/statistics' replace />} />
31+
<Route path='/index.html' element={<Navigate to='/statistics' replace />} />
32+
{menuItems.map(item => (
33+
<Route key={item.key} path={item.path} element={<item.component />} />
34+
))}
35+
</Routes>
36+
</ConfigLoader>
37+
</div>
38+
</div>
39+
</AuthWrapper>
40+
</div>
41+
);
42+
}
3243

3344
function App() {
3445
const basename = getBasename();
3546
return (
3647
<Provider store={store}>
3748
<BrowserRouter basename={basename}>
38-
<div className='app'>
39-
<AuthWrapper>
40-
<div className='appLayout'>
41-
<Menu />
42-
<div className='mainContent'>
43-
<ConfigLoader>
44-
<Routes>
45-
<Route path='/' element={<Navigate to='/statistics' replace />} />
46-
<Route path='/index.html' element={<Navigate to='/statistics' replace />} />
47-
{menuItems.map(item => (
48-
<Route key={item.key} path={item.path} element={<item.component />} />
49-
))}
50-
</Routes>
51-
</ConfigLoader>
52-
</div>
53-
</div>
54-
</AuthWrapper>
55-
</div>
49+
<AppContent />
5650
</BrowserRouter>
5751
</Provider>
5852
);

AdminPanel/client/src/api/index.js

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL ?? '';
2-
const API_BASE_PATH = '/api/v1/admin';
1+
import {getApiBasePath, getDocServicePath} from '../utils/paths';
2+
3+
const API_BASE_PATH = getApiBasePath();
4+
const DOCSERVICE_URL = getDocServicePath();
35

46
const isNetworkError = error => {
57
if (!error) return false;
@@ -21,33 +23,33 @@ const safeFetch = async (url, options = {}) => {
2123
};
2224

2325
export const fetchStatistics = async () => {
24-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/stat`);
26+
const response = await safeFetch(`${API_BASE_PATH}/stat`, {credentials: 'include'});
2527
if (!response.ok) throw new Error('Failed to fetch statistics');
2628
return response.json();
2729
};
2830

2931
export const fetchConfiguration = async () => {
30-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config`, {credentials: 'include'});
32+
const response = await safeFetch(`${API_BASE_PATH}/config`, {credentials: 'include'});
3133
if (response.status === 401) throw new Error('UNAUTHORIZED');
3234
if (!response.ok) throw new Error('Failed to fetch configuration');
3335
return response.json();
3436
};
3537

3638
export const fetchConfigurationSchema = async () => {
37-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config/schema`, {credentials: 'include'});
38-
if (response.status === 401) throw new Error('UNAUTHORIZED');
39+
const response = await safeFetch(`${API_BASE_PATH}/config/schema`, {credentials: 'include'});
3940
if (!response.ok) throw new Error('Failed to fetch configuration schema');
4041
return response.json();
4142
};
4243

4344
export const updateConfiguration = async configData => {
44-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config`, {
45+
const response = await safeFetch(`${API_BASE_PATH}/config`, {
4546
method: 'PATCH',
4647
headers: {'Content-Type': 'application/json'},
4748
credentials: 'include',
4849
body: JSON.stringify(configData)
4950
});
5051
if (!response.ok) {
52+
if (response.status === 401) throw new Error('UNAUTHORIZED');
5153
let errorMessage = 'Configuration update failed';
5254
try {
5355
const errorData = await response.json();
@@ -61,23 +63,22 @@ export const updateConfiguration = async configData => {
6163
};
6264

6365
export const fetchCurrentUser = async () => {
64-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/me`, {credentials: 'include'});
65-
if (!response.ok) throw new Error('Failed to fetch current user');
66+
const response = await safeFetch(`${API_BASE_PATH}/me`, {credentials: 'include'});
6667
const data = await response.json();
6768
if (data && data.authorized === false) {
68-
throw new Error('Unauthorized');
69+
throw new Error('UNAUTHORIZED');
6970
}
7071
return data;
7172
};
7273

7374
export const checkSetupRequired = async () => {
74-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/setup/required`, {credentials: 'include'});
75+
const response = await safeFetch(`${API_BASE_PATH}/setup/required`, {credentials: 'include'});
7576
if (!response.ok) throw new Error('Failed to check setup status');
7677
return response.json();
7778
};
7879

7980
export const setupAdminPassword = async ({bootstrapToken, password}) => {
80-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/setup`, {
81+
const response = await safeFetch(`${API_BASE_PATH}/setup`, {
8182
method: 'POST',
8283
headers: {'Content-Type': 'application/json'},
8384
credentials: 'include',
@@ -97,7 +98,7 @@ export const setupAdminPassword = async ({bootstrapToken, password}) => {
9798
};
9899

99100
export const login = async password => {
100-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/login`, {
101+
const response = await safeFetch(`${API_BASE_PATH}/login`, {
101102
method: 'POST',
102103
headers: {'Content-Type': 'application/json'},
103104
credentials: 'include',
@@ -120,13 +121,14 @@ export const login = async password => {
120121
};
121122

122123
export const changePassword = async ({currentPassword, newPassword}) => {
123-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/change-password`, {
124+
const response = await safeFetch(`${API_BASE_PATH}/change-password`, {
124125
method: 'POST',
125126
headers: {'Content-Type': 'application/json'},
126127
credentials: 'include',
127128
body: JSON.stringify({currentPassword, newPassword})
128129
});
129130
if (!response.ok) {
131+
if (response.status === 401) throw new Error('UNAUTHORIZED');
130132
let errorMessage = 'Password change failed';
131133
try {
132134
const errorData = await response.json();
@@ -140,7 +142,7 @@ export const changePassword = async ({currentPassword, newPassword}) => {
140142
};
141143

142144
export const logout = async () => {
143-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/logout`, {
145+
const response = await safeFetch(`${API_BASE_PATH}/logout`, {
144146
method: 'POST',
145147
headers: {'Content-Type': 'application/json'},
146148
credentials: 'include'
@@ -150,12 +152,13 @@ export const logout = async () => {
150152
};
151153

152154
export const rotateWopiKeys = async () => {
153-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/wopi/rotate-keys`, {
155+
const response = await safeFetch(`${API_BASE_PATH}/wopi/rotate-keys`, {
154156
method: 'POST',
155157
headers: {'Content-Type': 'application/json'},
156158
credentials: 'include'
157159
});
158160
if (!response.ok) {
161+
if (response.status === 401) throw new Error('UNAUTHORIZED');
159162
let errorMessage = 'Failed to rotate WOPI keys';
160163
try {
161164
const errorData = await response.json();
@@ -169,26 +172,31 @@ export const rotateWopiKeys = async () => {
169172
};
170173

171174
export const checkHealth = async () => {
172-
const url = process.env.NODE_ENV === 'development' ? '/healthcheck-api' : '../healthcheck';
173-
const response = await safeFetch(url);
175+
const response = await safeFetch(`${DOCSERVICE_URL}/healthcheck`);
174176
if (!response.ok) throw new Error('DocService health check failed');
175177
const result = await response.text();
176178
if (result !== 'true') throw new Error('DocService health check failed');
177179
return true;
178180
};
179181

180-
export const resetConfiguration = async () => {
181-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/config/reset`, {
182+
export const resetConfiguration = async (paths = ['*']) => {
183+
const pathsArray = Array.isArray(paths) ? paths : [paths];
184+
185+
const response = await safeFetch(`${API_BASE_PATH}/config/reset`, {
182186
method: 'POST',
183187
headers: {'Content-Type': 'application/json'},
184-
credentials: 'include'
188+
credentials: 'include',
189+
body: JSON.stringify({paths: pathsArray})
185190
});
186-
if (!response.ok) throw new Error('Failed to reset configuration');
191+
if (!response.ok) {
192+
if (response.status === 401) throw new Error('UNAUTHORIZED');
193+
throw new Error('Failed to reset configuration');
194+
}
187195
return response.json();
188196
};
189197

190198
export const generateDocServerToken = async body => {
191-
const response = await safeFetch(`${BACKEND_URL}${API_BASE_PATH}/generate-docserver-token`, {
199+
const response = await safeFetch(`${API_BASE_PATH}/generate-docserver-token`, {
192200
method: 'POST',
193201
headers: {'Content-Type': 'application/json'},
194202
credentials: 'include',
@@ -204,8 +212,7 @@ const callCommandService = async body => {
204212
const {token} = await generateDocServerToken(body);
205213
body.token = token;
206214

207-
const url = process.env.REACT_APP_DOCSERVICE_URL ? `${process.env.REACT_APP_DOCSERVICE_URL}/command` : '../command';
208-
const response = await safeFetch(url, {
215+
const response = await safeFetch(`${DOCSERVICE_URL}/command`, {
209216
method: 'POST',
210217
headers: {
211218
'Content-Type': 'application/json',

AdminPanel/client/src/assets/AppMenuSprite.svg

Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 2 additions & 2 deletions
Loading

AdminPanel/client/src/assets/File.svg

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)