dsh-linked-workspace 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.en.md +16 -1
- package/README.md +16 -1
- package/docs/API.md +6 -6
- package/docs/VERIFY.md +22 -0
- package/lib/client.js +535 -65
- package/lib/index.js +138 -40
- package/lib/linked.js +46 -24
- package/package.json +2 -2
- package/scripts/doc_tool.py +10 -3
package/lib/client.js
CHANGED
|
@@ -1835,8 +1835,112 @@ function fileIconSpec(name, isDir) {
|
|
|
1835
1835
|
return ICON_spec('unknown')
|
|
1836
1836
|
}
|
|
1837
1837
|
|
|
1838
|
+
// ---- file-creation.js ----
|
|
1839
|
+
// Shared by the directory menu, its submit handler and API regression tests.
|
|
1840
|
+
// Preview formats (CSV/HTML/images) are regular files; only document templates
|
|
1841
|
+
// are sent to docs.create.
|
|
1842
|
+
function newEntryMenuItems(menu, t, onNew) {
|
|
1843
|
+
return [true, false].map(function (isDir) {
|
|
1844
|
+
var title = t(isDir ? 'newFolderTitle' : 'newFileTitle');
|
|
1845
|
+
return {
|
|
1846
|
+
label: t(isDir ? 'ctxNewFolder' : 'ctxNewFile'),
|
|
1847
|
+
onSelect: function () {
|
|
1848
|
+
onNew({ kind: 'new', side: menu.side, dir: menu.path, isDir: isDir,
|
|
1849
|
+
title: title, label: t('nameLabel'), value: title });
|
|
1850
|
+
},
|
|
1851
|
+
};
|
|
1852
|
+
});
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
function newEntryRequest(side, dirPath, name, isDir, scope) {
|
|
1856
|
+
var child = String(name || '').trim();
|
|
1857
|
+
if (!child || child === '.' || child === '..' || /[/\\\0]/.test(child)) {
|
|
1858
|
+
var error = new Error('文件或文件夹名称无效 / Invalid file or folder name');
|
|
1859
|
+
error.code = 'BAD_REQUEST';
|
|
1860
|
+
throw error;
|
|
1861
|
+
}
|
|
1862
|
+
var parent = String(dirPath || '');
|
|
1863
|
+
var separator = side === 'remote' || parent.indexOf('\\') < 0 ? '/' : '\\';
|
|
1864
|
+
var target = parent + (/[\\/]$/.test(parent) ? '' : separator) + child;
|
|
1865
|
+
var match = child.toLowerCase().match(/\.[^./\\]+$/);
|
|
1866
|
+
var extension = match ? match[0] : '';
|
|
1867
|
+
var kinds = { '.md': 'markdown', '.markdown': 'markdown', '.pdf': 'pdf',
|
|
1868
|
+
'.docx': 'docx', '.xlsx': 'xlsx', '.pptx': 'pptx' };
|
|
1869
|
+
var kind = isDir ? '' : (kinds[extension] || '');
|
|
1870
|
+
var method;
|
|
1871
|
+
var params;
|
|
1872
|
+
if (isDir) {
|
|
1873
|
+
method = side === 'remote' ? 'fs.mkdir' : 'local.mkdir';
|
|
1874
|
+
params = side === 'remote' ? { path: target, createOnly: true } : { path: parent, name: child };
|
|
1875
|
+
} else if (kind) {
|
|
1876
|
+
method = 'docs.create';
|
|
1877
|
+
params = { path: target, kind: kind };
|
|
1878
|
+
} else {
|
|
1879
|
+
method = side === 'remote' ? 'fs.write' : 'local.newFile';
|
|
1880
|
+
params = side === 'remote' ? { path: target, content: '', createOnly: true } : { path: target };
|
|
1881
|
+
}
|
|
1882
|
+
if (side === 'remote') params = Object.assign({}, scope, params);
|
|
1883
|
+
return { method: method, params: params, path: target, kind: kind };
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
// ---- url-safety.js ----
|
|
1887
|
+
// URL safety helpers shared by the document sanitizer and its unit tests.
|
|
1888
|
+
//
|
|
1889
|
+
// Browsers strip ASCII control characters, tabs and newlines while parsing a
|
|
1890
|
+
// URL scheme, so `jav	ascript:` reaches the address bar as `javascript:`.
|
|
1891
|
+
// The checks below normalise the value the same way before looking at it.
|
|
1892
|
+
var SAFE_SCHEMES = { http: 1, https: 1, mailto: 1, tel: 1 };
|
|
1893
|
+
var IMAGE_DATA = /^data:image\/(?:png|jpeg|jpg|gif|webp|bmp|avif|x-icon)[;,]/
|
|
1894
|
+
var URL_ATTRIBUTES = {
|
|
1895
|
+
href: 1,
|
|
1896
|
+
src: 1,
|
|
1897
|
+
'xlink:href': 1,
|
|
1898
|
+
action: 1,
|
|
1899
|
+
formaction: 1,
|
|
1900
|
+
data: 1,
|
|
1901
|
+
poster: 1,
|
|
1902
|
+
background: 1,
|
|
1903
|
+
cite: 1,
|
|
1904
|
+
longdesc: 1,
|
|
1905
|
+
usemap: 1,
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
function normalizeUrlValue(value) {
|
|
1909
|
+
return String(value == null ? '' : value)
|
|
1910
|
+
// C0/C1 controls, space, zero-width and line/paragraph separators are what
|
|
1911
|
+
// URL parsers silently drop inside a scheme.
|
|
1912
|
+
.replace(/[\u0000-\u0020\u007f-\u009f\u200b-\u200f\u2028\u2029\ufeff]/g, '')
|
|
1913
|
+
.trim();
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
function urlSchemeOf(value) {
|
|
1917
|
+
var text = normalizeUrlValue(value);
|
|
1918
|
+
var match = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(text);
|
|
1919
|
+
return match ? match[1].toLowerCase() : '';
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
// Returns '' when the attribute is safe to keep, or a short reason when the
|
|
1923
|
+
// sanitizer has to drop it. `tagName` is only needed for data: URLs, which are
|
|
1924
|
+
// allowed for images and rejected everywhere else.
|
|
1925
|
+
function urlAttributeViolation(name, value, tagName) {
|
|
1926
|
+
var attribute = String(name || '').toLowerCase();
|
|
1927
|
+
if (/^on/.test(attribute)) return 'event handler';
|
|
1928
|
+
if (!URL_ATTRIBUTES[attribute]) return '';
|
|
1929
|
+
var text = normalizeUrlValue(value);
|
|
1930
|
+
if (text === '') return '';
|
|
1931
|
+
// Relative paths, fragments and protocol-relative URLs have no scheme.
|
|
1932
|
+
if (text.charAt(0) === '#' || text.slice(0, 2) === '//') return '';
|
|
1933
|
+
var scheme = urlSchemeOf(text);
|
|
1934
|
+
if (scheme === '') return '';
|
|
1935
|
+
if (SAFE_SCHEMES[scheme]) return '';
|
|
1936
|
+
if (scheme === 'data' && String(tagName || '').toLowerCase() === 'img' && IMAGE_DATA.test(text)) return '';
|
|
1937
|
+
return scheme + ': URL';
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1838
1940
|
var TREE_TAB_TYPE = 'dsh-linked-workspace';
|
|
1839
1941
|
var REMOTE_TAB_TYPE = 'dsh-linked-workspace:remote';
|
|
1942
|
+
// One tree page; a folder with 20k entries must not render 20k rows.
|
|
1943
|
+
var TREE_PAGE_SIZE = 500;
|
|
1840
1944
|
var linkedLocalKeys = new Set();
|
|
1841
1945
|
var linkedWorkspaceKeys = new Set();
|
|
1842
1946
|
var hasRemoteDirectories = false;
|
|
@@ -2006,6 +2110,12 @@ function fileIconSpec(name, isDir) {
|
|
|
2006
2110
|
truncated: '内容已截断',
|
|
2007
2111
|
fileLoadError: '无法打开文件',
|
|
2008
2112
|
imageLoadError: '无法预览图片',
|
|
2113
|
+
imageZoomIn: '放大',
|
|
2114
|
+
imageZoomOut: '缩小',
|
|
2115
|
+
imageFit: '适应窗口',
|
|
2116
|
+
imageActualSize: '原始大小',
|
|
2117
|
+
htmlLoadError: '无法预览网页',
|
|
2118
|
+
htmlScriptsDisabled: '网页脚本已禁用',
|
|
2009
2119
|
previewOnly: '图片预览',
|
|
2010
2120
|
csvTable: '表格',
|
|
2011
2121
|
csvSource: '源码',
|
|
@@ -2044,6 +2154,10 @@ function fileIconSpec(name, isDir) {
|
|
|
2044
2154
|
latency: '延迟',
|
|
2045
2155
|
countSuffix: ' 个',
|
|
2046
2156
|
noEntries: '空目录',
|
|
2157
|
+
loadMore: '加载更多(剩余 {{count}} 项)',
|
|
2158
|
+
loadMoreShort: '加载更多',
|
|
2159
|
+
sheetTruncated: '仅显示前 {{rows}} 行 × {{cols}} 列',
|
|
2160
|
+
sheetMore: '工作表较多,仅显示前 20 个',
|
|
2047
2161
|
noCredential: '缺少凭据',
|
|
2048
2162
|
sourceManagerShort: 'SSH',
|
|
2049
2163
|
remoteGroup: '远程目录',
|
|
@@ -2329,6 +2443,12 @@ function fileIconSpec(name, isDir) {
|
|
|
2329
2443
|
truncated: 'Content truncated',
|
|
2330
2444
|
fileLoadError: 'Cannot open the file',
|
|
2331
2445
|
imageLoadError: 'Cannot preview this image',
|
|
2446
|
+
imageZoomIn: 'Zoom in',
|
|
2447
|
+
imageZoomOut: 'Zoom out',
|
|
2448
|
+
imageFit: 'Fit to window',
|
|
2449
|
+
imageActualSize: 'Actual size',
|
|
2450
|
+
htmlLoadError: 'Cannot preview this page',
|
|
2451
|
+
htmlScriptsDisabled: 'Page scripts are disabled',
|
|
2332
2452
|
previewOnly: 'Image preview',
|
|
2333
2453
|
csvTable: 'Table',
|
|
2334
2454
|
csvSource: 'Source',
|
|
@@ -2367,6 +2487,10 @@ function fileIconSpec(name, isDir) {
|
|
|
2367
2487
|
latency: 'Latency',
|
|
2368
2488
|
countSuffix: '',
|
|
2369
2489
|
noEntries: 'Empty directory',
|
|
2490
|
+
loadMore: 'Load more ({{count}} left)',
|
|
2491
|
+
loadMoreShort: 'Load more',
|
|
2492
|
+
sheetTruncated: 'Showing the first {{rows}} rows × {{cols}} columns',
|
|
2493
|
+
sheetMore: 'Showing the first 20 worksheets only',
|
|
2370
2494
|
noCredential: 'No credentials',
|
|
2371
2495
|
sourceManagerShort: 'SSH',
|
|
2372
2496
|
remoteGroup: 'Remote directories',
|
|
@@ -2677,6 +2801,7 @@ function fileIconSpec(name, isDir) {
|
|
|
2677
2801
|
FS_IS_DIR: '目标是目录',
|
|
2678
2802
|
FS_NOT_DIR: '目标不是目录',
|
|
2679
2803
|
FS_TOO_LARGE: '文件过大',
|
|
2804
|
+
FS_EXISTS: '目标已存在,请使用其他名称',
|
|
2680
2805
|
CONFLICT: '远端文件已被修改',
|
|
2681
2806
|
BAD_REQUEST: '请求参数无效',
|
|
2682
2807
|
},
|
|
@@ -2691,6 +2816,7 @@ function fileIconSpec(name, isDir) {
|
|
|
2691
2816
|
FS_IS_DIR: 'That path is a directory',
|
|
2692
2817
|
FS_NOT_DIR: 'That path is not a directory',
|
|
2693
2818
|
FS_TOO_LARGE: 'The file is too large',
|
|
2819
|
+
FS_EXISTS: 'The target already exists; choose another name',
|
|
2694
2820
|
CONFLICT: 'The remote file changed',
|
|
2695
2821
|
BAD_REQUEST: 'Invalid request',
|
|
2696
2822
|
},
|
|
@@ -2713,6 +2839,7 @@ function fileIconSpec(name, isDir) {
|
|
|
2713
2839
|
if (value === '.xlsx') return 'xlsx';
|
|
2714
2840
|
if (value === '.pptx') return 'pptx';
|
|
2715
2841
|
if (value === '.csv' || value === '.tsv') return 'csv';
|
|
2842
|
+
if (value === '.html' || value === '.htm') return 'html';
|
|
2716
2843
|
if (value === '.png' || value === '.jpg' || value === '.jpeg' || value === '.gif'
|
|
2717
2844
|
|| value === '.webp' || value === '.bmp' || value === '.ico' || value === '.avif'
|
|
2718
2845
|
|| value === '.svg') return 'image';
|
|
@@ -2770,7 +2897,7 @@ function fileIconSpec(name, isDir) {
|
|
|
2770
2897
|
parsed.querySelectorAll('script,style,iframe,object,embed,link,meta,base,form').forEach(function (node) { node.remove(); });
|
|
2771
2898
|
parsed.querySelectorAll('*').forEach(function (element) {
|
|
2772
2899
|
Array.from(element.attributes).forEach(function (attribute) {
|
|
2773
|
-
if (
|
|
2900
|
+
if (urlAttributeViolation(attribute.name, attribute.value, element.tagName)) {
|
|
2774
2901
|
element.removeAttribute(attribute.name);
|
|
2775
2902
|
}
|
|
2776
2903
|
});
|
|
@@ -3036,6 +3163,9 @@ function fileIconSpec(name, isDir) {
|
|
|
3036
3163
|
'.rw-tree-body{padding:2px 4px 6px;border-top:1px solid var(--dsw-alias-border-l1)}',
|
|
3037
3164
|
'.rw-row{display:flex;align-items:center;gap:5px;width:100%;min-width:0;height:23px;padding:0 6px;border:0;border-radius:5px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:12.5px;cursor:pointer;text-align:left;box-sizing:border-box}',
|
|
3038
3165
|
'.rw-row:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(127,127,127,.12))}',
|
|
3166
|
+
'.rw-row:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4176e6));outline-offset:-1px}',
|
|
3167
|
+
'.rw-input:focus-visible,.rw-textarea:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4176e6));outline-offset:-1px}',
|
|
3168
|
+
'.rw-btn:focus-visible,.rw-doc-tab:focus-visible,.rw-icon-btn:focus-visible{outline:var(--dsw-focus-ring-width,2px) solid var(--dsw-focus-ring-color,var(--dsw-alias-brand-primary,#4176e6));outline-offset:1px}',
|
|
3039
3169
|
'.rw-row.is-active{background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent)}',
|
|
3040
3170
|
'.rw-row-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1 1 auto;min-width:0}',
|
|
3041
3171
|
'.rw-row-size{flex:none;font-size:10.5px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
|
|
@@ -3163,6 +3293,10 @@ function fileIconSpec(name, isDir) {
|
|
|
3163
3293
|
'.rw-doc-table td{border:1px solid var(--dsw-alias-border-l2);padding:0}',
|
|
3164
3294
|
'.rw-doc-table input{width:100%;border:0;background:transparent;padding:4px 5px;font:inherit;color:var(--dsw-alias-label-primary);outline:none;box-sizing:border-box}',
|
|
3165
3295
|
'.rw-doc-tabs{display:flex;gap:3px;padding:5px;border-bottom:1px solid var(--dsw-alias-border-l1);overflow:auto;flex:none}',
|
|
3296
|
+
'.rw-doc-note{padding:3px 10px;font-size:11px;color:var(--dsw-alias-label-tertiary);border-bottom:1px solid var(--dsw-alias-border-l1)}',
|
|
3297
|
+
'.rw-load-more{width:100%;border:0;background:transparent;cursor:pointer;text-align:left;font:inherit;font-size:11.5px;color:var(--dsw-alias-brand-primary)}',
|
|
3298
|
+
'.rw-load-more:hover{text-decoration:underline}',
|
|
3299
|
+
'.rw-load-more:disabled{opacity:.6;cursor:default}',
|
|
3166
3300
|
'.rw-doc-tab{padding:2px 8px;border:1px solid transparent;border-radius:5px;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11.5px;cursor:pointer;white-space:nowrap}',
|
|
3167
3301
|
'.rw-doc-tab.is-on{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary)}',
|
|
3168
3302
|
'.rw-doc-grid{width:100%;border-collapse:collapse;font-size:11.5px}',
|
|
@@ -3213,8 +3347,14 @@ function fileIconSpec(name, isDir) {
|
|
|
3213
3347
|
'.rw-image-preview{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}',
|
|
3214
3348
|
'.rw-image-head{display:flex;align-items:center;gap:8px;padding:6px 10px;border-bottom:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);min-width:0;flex:none}',
|
|
3215
3349
|
'.rw-image-head .rw-actions{display:flex;align-items:center;gap:6px;flex:none}',
|
|
3216
|
-
'.rw-image-stage{display:
|
|
3217
|
-
'.rw-image{
|
|
3350
|
+
'.rw-image-stage{display:block;flex:1 1 auto;min-height:0;overflow:auto;padding:18px;text-align:center;background:repeating-conic-gradient(var(--dsw-alias-bg-layer-1) 0% 25%,var(--dsw-alias-bg-base) 0% 50%) 50%/18px 18px}',
|
|
3351
|
+
'.rw-image-canvas{display:inline-block;min-width:100%;min-height:100%;box-sizing:border-box}',
|
|
3352
|
+
'.rw-image{display:inline-block;max-width:none;max-height:none;vertical-align:top;border-radius:4px;box-shadow:0 2px 16px rgba(0,0,0,.18)}',
|
|
3353
|
+
'.rw-image-tools{display:flex;align-items:center;gap:4px;flex:none}',
|
|
3354
|
+
'.rw-image-zoom{min-width:52px;text-align:center;font-variant-numeric:tabular-nums;color:var(--dsw-alias-label-secondary);font-size:12px}',
|
|
3355
|
+
'.rw-html-preview{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}',
|
|
3356
|
+
'.rw-html-stage{flex:1 1 auto;min-height:0;background:#fff;position:relative}',
|
|
3357
|
+
'.rw-html-frame{display:block;width:100%;height:100%;border:0;background:#fff}',
|
|
3218
3358
|
'.rw-csv-wrap{flex:1 1 auto;min-height:0;overflow:auto;background:var(--dsw-alias-bg-base)}',
|
|
3219
3359
|
'.rw-csv-table{border-collapse:collapse;font:12px/18px ui-monospace,"Cascadia Code",Consolas,monospace;min-width:100%}',
|
|
3220
3360
|
'.rw-csv-table th{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);font-weight:600;text-align:left;padding:4px 8px;border:1px solid var(--dsw-alias-border-l1);white-space:nowrap}',
|
|
@@ -3866,13 +4006,38 @@ function fileIconSpec(name, isDir) {
|
|
|
3866
4006
|
var state = useState({ loading: true, error: null, data: null });
|
|
3867
4007
|
var view = state[0];
|
|
3868
4008
|
var setView = state[1];
|
|
4009
|
+
var naturalState = useState(null);
|
|
4010
|
+
var natural = naturalState[0];
|
|
4011
|
+
var setNatural = naturalState[1];
|
|
4012
|
+
var zoomState = useState(1);
|
|
4013
|
+
var zoom = zoomState[0];
|
|
4014
|
+
var setZoom = zoomState[1];
|
|
4015
|
+
var stageRef = useRef(null);
|
|
3869
4016
|
var params = scopeParams(scope);
|
|
4017
|
+
var MIN_ZOOM = 0.05;
|
|
4018
|
+
var MAX_ZOOM = 8;
|
|
4019
|
+
|
|
4020
|
+
function clampZoom(value) {
|
|
4021
|
+
return Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, value));
|
|
4022
|
+
}
|
|
4023
|
+
|
|
4024
|
+
function zoomBy(factor) {
|
|
4025
|
+
setZoom(function (previous) { return clampZoom(previous * factor); });
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
function fitToStage() {
|
|
4029
|
+
if (!natural || !stageRef.current) return;
|
|
4030
|
+
var width = Math.max(40, stageRef.current.clientWidth - 36);
|
|
4031
|
+
var height = Math.max(40, stageRef.current.clientHeight - 36);
|
|
4032
|
+
setZoom(clampZoom(Math.min(1, width / natural.width, height / natural.height)));
|
|
4033
|
+
}
|
|
3870
4034
|
|
|
3871
4035
|
var load = useCallback(function () {
|
|
3872
4036
|
if (!filePath || (!local && !scope)) {
|
|
3873
4037
|
setView({ loading: false, error: new Error(t('noWorkspace')), data: null });
|
|
3874
4038
|
return;
|
|
3875
4039
|
}
|
|
4040
|
+
setNatural(null);
|
|
3876
4041
|
setView({ loading: true, error: null, data: null });
|
|
3877
4042
|
var request = local ? { path: filePath } : Object.assign({ path: filePath }, params);
|
|
3878
4043
|
apiCall(local ? 'local.media' : 'media.read', request).then(function (result) {
|
|
@@ -3884,13 +4049,59 @@ function fileIconSpec(name, isDir) {
|
|
|
3884
4049
|
|
|
3885
4050
|
useEffect(function () { load(); }, [load]);
|
|
3886
4051
|
|
|
4052
|
+
useEffect(function () {
|
|
4053
|
+
setZoom(1);
|
|
4054
|
+
setNatural(null);
|
|
4055
|
+
}, [filePath]);
|
|
4056
|
+
|
|
4057
|
+
useEffect(function () {
|
|
4058
|
+
if (!natural || !stageRef.current) return;
|
|
4059
|
+
var width = Math.max(40, stageRef.current.clientWidth - 36);
|
|
4060
|
+
var height = Math.max(40, stageRef.current.clientHeight - 36);
|
|
4061
|
+
setZoom(clampZoom(Math.min(1, width / natural.width, height / natural.height)));
|
|
4062
|
+
}, [natural]);
|
|
4063
|
+
|
|
4064
|
+
useEffect(function () {
|
|
4065
|
+
var stage = stageRef.current;
|
|
4066
|
+
if (!stage) return;
|
|
4067
|
+
function onWheel(event) {
|
|
4068
|
+
if (!event.ctrlKey && !event.metaKey) return;
|
|
4069
|
+
event.preventDefault();
|
|
4070
|
+
setZoom(function (previous) {
|
|
4071
|
+
return clampZoom(previous * (event.deltaY < 0 ? 1.2 : 1 / 1.2));
|
|
4072
|
+
});
|
|
4073
|
+
}
|
|
4074
|
+
stage.addEventListener('wheel', onWheel, { passive: false });
|
|
4075
|
+
return function () { stage.removeEventListener('wheel', onWheel); };
|
|
4076
|
+
}, [view.data]);
|
|
4077
|
+
|
|
3887
4078
|
var name = basename(filePath) || t('fileTabTitle');
|
|
4079
|
+
var zoomTools = view.data ? h('div', { className: 'rw-image-tools' },
|
|
4080
|
+
h('button', {
|
|
4081
|
+
type: 'button', className: 'rw-btn rw-btn-sm', title: t('imageZoomOut'),
|
|
4082
|
+
onClick: function () { zoomBy(1 / 1.2); },
|
|
4083
|
+
}, '−'),
|
|
4084
|
+
h('span', { className: 'rw-image-zoom' }, Math.round(zoom * 100) + '%'),
|
|
4085
|
+
h('button', {
|
|
4086
|
+
type: 'button', className: 'rw-btn rw-btn-sm', title: t('imageZoomIn'),
|
|
4087
|
+
onClick: function () { zoomBy(1.2); },
|
|
4088
|
+
}, '+'),
|
|
4089
|
+
h('button', {
|
|
4090
|
+
type: 'button', className: 'rw-btn rw-btn-sm', title: t('imageFit'),
|
|
4091
|
+
onClick: fitToStage,
|
|
4092
|
+
}, t('imageFit')),
|
|
4093
|
+
h('button', {
|
|
4094
|
+
type: 'button', className: 'rw-btn rw-btn-sm', title: t('imageActualSize'),
|
|
4095
|
+
onClick: function () { setZoom(1); },
|
|
4096
|
+
}, '1:1')) : null;
|
|
4097
|
+
|
|
3888
4098
|
var header = h('div', { className: 'rw-image-head' },
|
|
3889
4099
|
h(FileGlyph, { name: name, size: 15 }),
|
|
3890
4100
|
h('div', { className: 'rw-editor-title' },
|
|
3891
4101
|
h('span', { className: 'rw-editor-name', title: name }, name),
|
|
3892
4102
|
h('span', { className: 'rw-editor-path', title: filePath }, filePath)),
|
|
3893
4103
|
h('div', { className: 'rw-actions' },
|
|
4104
|
+
zoomTools,
|
|
3894
4105
|
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', onClick: load, disabled: view.loading }, IconRefresh(12), t('reload'))));
|
|
3895
4106
|
|
|
3896
4107
|
if (view.loading) {
|
|
@@ -3904,24 +4115,104 @@ function fileIconSpec(name, isDir) {
|
|
|
3904
4115
|
h('button', { type: 'button', className: 'rw-btn', onClick: load }, IconRefresh(13), t('retry'))));
|
|
3905
4116
|
}
|
|
3906
4117
|
var data = view.data;
|
|
4118
|
+
var imageStyle = natural
|
|
4119
|
+
? { width: natural.width * zoom + 'px', height: natural.height * zoom + 'px' }
|
|
4120
|
+
: { maxWidth: '100%', maxHeight: '100%' };
|
|
3907
4121
|
return h('div', { className: 'rw-image-preview' },
|
|
3908
4122
|
header,
|
|
3909
|
-
h('div', { className: 'rw-image-stage' },
|
|
3910
|
-
h('
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
4123
|
+
h('div', { className: 'rw-image-stage', ref: stageRef },
|
|
4124
|
+
h('div', { className: 'rw-image-canvas' },
|
|
4125
|
+
h('img', {
|
|
4126
|
+
className: 'rw-image',
|
|
4127
|
+
src: 'data:' + data.mime + ';base64,' + data.base64,
|
|
4128
|
+
alt: name,
|
|
4129
|
+
style: imageStyle,
|
|
4130
|
+
onLoad: function (event) {
|
|
4131
|
+
var image = event.currentTarget;
|
|
4132
|
+
if (image.naturalWidth && image.naturalHeight) {
|
|
4133
|
+
setNatural({ width: image.naturalWidth, height: image.naturalHeight });
|
|
4134
|
+
}
|
|
4135
|
+
},
|
|
4136
|
+
onError: function () {
|
|
4137
|
+
setView({ loading: false, error: new Error(t('imageLoadError')), data: null });
|
|
4138
|
+
},
|
|
4139
|
+
}))),
|
|
3918
4140
|
h('div', { className: 'rw-editor-status' },
|
|
3919
4141
|
h('span', { className: 'rw-status-chip' }, data.mime || 'image'),
|
|
3920
4142
|
h('span', { className: 'rw-status-chip' }, formatBytes(data.size || 0)),
|
|
4143
|
+
h('span', { className: 'rw-status-chip' }, Math.round(zoom * 100) + '%'),
|
|
3921
4144
|
h('span', { className: 'rw-fill', style: { flex: '1 1 auto' } }),
|
|
3922
4145
|
h('span', { className: 'rw-status-chip' }, t('previewOnly'))));
|
|
3923
4146
|
}
|
|
3924
4147
|
|
|
4148
|
+
function HtmlPreview(props) {
|
|
4149
|
+
var ctx = props.ctx;
|
|
4150
|
+
var t = props.t;
|
|
4151
|
+
var locale = props.locale;
|
|
4152
|
+
var scope = props.scope;
|
|
4153
|
+
var local = props.local === true;
|
|
4154
|
+
var filePath = props.path;
|
|
4155
|
+
var state = useState({ loading: true, error: null, data: null });
|
|
4156
|
+
var view = state[0];
|
|
4157
|
+
var setView = state[1];
|
|
4158
|
+
var params = scopeParams(scope);
|
|
4159
|
+
|
|
4160
|
+
var load = useCallback(function () {
|
|
4161
|
+
if (!filePath || (!local && !scope)) {
|
|
4162
|
+
setView({ loading: false, error: new Error(t('noWorkspace')), data: null });
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4165
|
+
setView({ loading: true, error: null, data: null });
|
|
4166
|
+
var request = local ? { path: filePath } : Object.assign({ path: filePath }, params);
|
|
4167
|
+
apiCall(local ? 'local.read' : 'fs.read', request).then(function (result) {
|
|
4168
|
+
if (result.binary || result.tooLarge) {
|
|
4169
|
+
setView({ loading: false, error: new Error(t('htmlLoadError')), data: null });
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
setView({ loading: false, error: null, data: result });
|
|
4173
|
+
}).catch(function (error) {
|
|
4174
|
+
setView({ loading: false, error: error, data: null });
|
|
4175
|
+
});
|
|
4176
|
+
}, [scopeKeyOf(scope), filePath, local]);
|
|
4177
|
+
|
|
4178
|
+
useEffect(function () { load(); }, [load]);
|
|
4179
|
+
|
|
4180
|
+
var name = basename(filePath) || t('fileTabTitle');
|
|
4181
|
+
var header = h('div', { className: 'rw-image-head' },
|
|
4182
|
+
h(FileGlyph, { name: name, size: 15 }),
|
|
4183
|
+
h('div', { className: 'rw-editor-title' },
|
|
4184
|
+
h('span', { className: 'rw-editor-name', title: name }, name),
|
|
4185
|
+
h('span', { className: 'rw-editor-path', title: filePath }, filePath)),
|
|
4186
|
+
h('div', { className: 'rw-actions' },
|
|
4187
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', onClick: load, disabled: view.loading }, IconRefresh(12), t('reload'))));
|
|
4188
|
+
|
|
4189
|
+
if (view.loading) {
|
|
4190
|
+
return h('div', { className: 'rw-html-preview' }, header,
|
|
4191
|
+
h('div', { className: 'rw-editor-message' }, Spinner(14), t('loading')));
|
|
4192
|
+
}
|
|
4193
|
+
if (view.error) {
|
|
4194
|
+
return h('div', { className: 'rw-html-preview' }, header,
|
|
4195
|
+
h('div', { className: 'rw-editor-message' },
|
|
4196
|
+
h(Banner, { kind: 'error' }, t('htmlLoadError') + ':' + messageOf(view.error, t, locale)),
|
|
4197
|
+
h('button', { type: 'button', className: 'rw-btn', onClick: load }, IconRefresh(13), t('retry'))));
|
|
4198
|
+
}
|
|
4199
|
+
var data = view.data;
|
|
4200
|
+
return h('div', { className: 'rw-html-preview' },
|
|
4201
|
+
header,
|
|
4202
|
+
h('div', { className: 'rw-html-stage' },
|
|
4203
|
+
h('iframe', {
|
|
4204
|
+
className: 'rw-html-frame',
|
|
4205
|
+
title: name,
|
|
4206
|
+
sandbox: '',
|
|
4207
|
+
srcDoc: data.content || '',
|
|
4208
|
+
})),
|
|
4209
|
+
h('div', { className: 'rw-editor-status' },
|
|
4210
|
+
h('span', { className: 'rw-status-chip' }, 'HTML'),
|
|
4211
|
+
h('span', { className: 'rw-status-chip' }, formatBytes(data.size || 0)),
|
|
4212
|
+
h('span', { className: 'rw-fill', style: { flex: '1 1 auto' } }),
|
|
4213
|
+
h('span', { className: 'rw-status-chip' }, t('htmlScriptsDisabled'))));
|
|
4214
|
+
}
|
|
4215
|
+
|
|
3925
4216
|
// ── editor ─────────────────────────────────────────────────────────────
|
|
3926
4217
|
|
|
3927
4218
|
function colorForToken(colors, type) {
|
|
@@ -4337,6 +4628,19 @@ function fileIconSpec(name, isDir) {
|
|
|
4337
4628
|
});
|
|
4338
4629
|
}
|
|
4339
4630
|
|
|
4631
|
+
function listRequest(path, offset) {
|
|
4632
|
+
var params = { path: path, limit: TREE_PAGE_SIZE, offset: offset };
|
|
4633
|
+
return props.kind === 'local'
|
|
4634
|
+
? apiCall('local.list', params)
|
|
4635
|
+
: apiCall('fs.list', Object.assign(params, scopeParams(props.scope)));
|
|
4636
|
+
}
|
|
4637
|
+
|
|
4638
|
+
function applyEntries(list) {
|
|
4639
|
+
return props.showHidden ? list : list.filter(function (entry) {
|
|
4640
|
+
return String(entry.name || '').charAt(0) !== '.';
|
|
4641
|
+
});
|
|
4642
|
+
}
|
|
4643
|
+
|
|
4340
4644
|
function load(path, silent) {
|
|
4341
4645
|
if (pending.current.has(path)) return;
|
|
4342
4646
|
pending.current.add(path);
|
|
@@ -4346,18 +4650,18 @@ function fileIconSpec(name, isDir) {
|
|
|
4346
4650
|
entries.set(path, { loading: !silent, error: null, entries: existing.entries || [], loaded: existing.loaded });
|
|
4347
4651
|
return { entries: entries };
|
|
4348
4652
|
});
|
|
4349
|
-
|
|
4350
|
-
? apiCall('local.list', { path: path })
|
|
4351
|
-
: apiCall('fs.list', Object.assign({ path: path }, scopeParams(props.scope)));
|
|
4352
|
-
request.then(function (result) {
|
|
4653
|
+
listRequest(path, 0).then(function (result) {
|
|
4353
4654
|
pending.current.delete(path);
|
|
4354
|
-
var entries = result.entries || [];
|
|
4355
|
-
if (!props.showHidden) {
|
|
4356
|
-
entries = entries.filter(function (entry) { return String(entry.name || '').charAt(0) !== '.'; });
|
|
4357
|
-
}
|
|
4358
4655
|
setCache(function (previous) {
|
|
4359
4656
|
var next = new Map(previous.entries);
|
|
4360
|
-
next.set(path, {
|
|
4657
|
+
next.set(path, {
|
|
4658
|
+
loading: false,
|
|
4659
|
+
error: null,
|
|
4660
|
+
loaded: true,
|
|
4661
|
+
entries: applyEntries(result.entries || []),
|
|
4662
|
+
total: Number(result.total) || (result.entries || []).length,
|
|
4663
|
+
hasMore: Boolean(result.hasMore),
|
|
4664
|
+
});
|
|
4361
4665
|
return { entries: next };
|
|
4362
4666
|
});
|
|
4363
4667
|
setExpanded(function (previous) {
|
|
@@ -4375,7 +4679,39 @@ function fileIconSpec(name, isDir) {
|
|
|
4375
4679
|
});
|
|
4376
4680
|
}
|
|
4377
4681
|
|
|
4378
|
-
|
|
4682
|
+
function loadMore(path) {
|
|
4683
|
+
var record = cache.entries.get(path);
|
|
4684
|
+
if (!record || record.loadingMore || !record.hasMore) return;
|
|
4685
|
+
setCache(function (previous) {
|
|
4686
|
+
var next = new Map(previous.entries);
|
|
4687
|
+
var current = next.get(path);
|
|
4688
|
+
if (current) next.set(path, Object.assign({}, current, { loadingMore: true }));
|
|
4689
|
+
return { entries: next };
|
|
4690
|
+
});
|
|
4691
|
+
listRequest(path, (record.entries || []).length).then(function (result) {
|
|
4692
|
+
setCache(function (previous) {
|
|
4693
|
+
var next = new Map(previous.entries);
|
|
4694
|
+
var current = next.get(path) || { entries: [] };
|
|
4695
|
+
next.set(path, Object.assign({}, current, {
|
|
4696
|
+
loadingMore: false,
|
|
4697
|
+
loaded: true,
|
|
4698
|
+
entries: current.entries.concat(applyEntries(result.entries || [])),
|
|
4699
|
+
total: Number(result.total) || current.total,
|
|
4700
|
+
hasMore: Boolean(result.hasMore),
|
|
4701
|
+
}));
|
|
4702
|
+
return { entries: next };
|
|
4703
|
+
});
|
|
4704
|
+
}).catch(function (error) {
|
|
4705
|
+
setCache(function (previous) {
|
|
4706
|
+
var next = new Map(previous.entries);
|
|
4707
|
+
var current = next.get(path);
|
|
4708
|
+
if (current) next.set(path, Object.assign({}, current, { loadingMore: false, moreError: error }));
|
|
4709
|
+
return { entries: next };
|
|
4710
|
+
});
|
|
4711
|
+
});
|
|
4712
|
+
}
|
|
4713
|
+
|
|
4714
|
+
return h('div', { className: 'rw-tree', role: 'tree', 'aria-label': t('allFiles') }, h(TreeDirectory, {
|
|
4379
4715
|
path: rootPath,
|
|
4380
4716
|
name: props.rootName || t('allFiles'),
|
|
4381
4717
|
depth: 0,
|
|
@@ -4386,12 +4722,13 @@ function fileIconSpec(name, isDir) {
|
|
|
4386
4722
|
toggle: toggle,
|
|
4387
4723
|
state: { entries: cache.entries },
|
|
4388
4724
|
load: load,
|
|
4725
|
+
loadMore: loadMore,
|
|
4389
4726
|
onSelect: props.onSelect,
|
|
4390
4727
|
onPin: props.onPin,
|
|
4391
4728
|
onSelectPath: props.onSelectPath,
|
|
4392
4729
|
onContextMenu: props.onContextMenu,
|
|
4393
4730
|
selectedPath: props.selectedPath || '',
|
|
4394
|
-
});
|
|
4731
|
+
}));
|
|
4395
4732
|
}
|
|
4396
4733
|
|
|
4397
4734
|
function TreeSection(props) {
|
|
@@ -4713,20 +5050,46 @@ function fileIconSpec(name, isDir) {
|
|
|
4713
5050
|
var state = useState(props.value || '');
|
|
4714
5051
|
var value = state[0];
|
|
4715
5052
|
var setValue = state[1];
|
|
4716
|
-
|
|
4717
|
-
|
|
5053
|
+
var statusState = useState({ busy: false, error: '' });
|
|
5054
|
+
var status = statusState[0];
|
|
5055
|
+
var setStatus = statusState[1];
|
|
5056
|
+
var pendingRef = useRef(false);
|
|
5057
|
+
var mountedRef = useRef(true);
|
|
5058
|
+
useEffect(function () {
|
|
5059
|
+
mountedRef.current = true;
|
|
5060
|
+
return function () { mountedRef.current = false; };
|
|
5061
|
+
}, []);
|
|
5062
|
+
function close() { if (!pendingRef.current) props.onClose(); }
|
|
5063
|
+
function submit() {
|
|
5064
|
+
if (value.trim() === '' || pendingRef.current) return;
|
|
5065
|
+
pendingRef.current = true;
|
|
5066
|
+
setStatus({ busy: true, error: '' });
|
|
5067
|
+
Promise.resolve().then(function () { return props.onSubmit(value.trim()); }).then(function () {
|
|
5068
|
+
pendingRef.current = false;
|
|
5069
|
+
if (mountedRef.current) setStatus({ busy: false, error: '' });
|
|
5070
|
+
}).catch(function (error) {
|
|
5071
|
+
pendingRef.current = false;
|
|
5072
|
+
if (mountedRef.current) setStatus({ busy: false, error: messageOf(error, props.t, props.locale) });
|
|
5073
|
+
});
|
|
5074
|
+
}
|
|
5075
|
+
return h(DialogShell, { title: props.title, onClose: close },
|
|
4718
5076
|
h('label', { className: 'rw-dialog-label' }, props.label),
|
|
4719
5077
|
h('input', {
|
|
4720
5078
|
className: 'rw-dialog-input',
|
|
4721
5079
|
value: value,
|
|
5080
|
+
disabled: status.busy,
|
|
4722
5081
|
autoFocus: true,
|
|
4723
5082
|
spellCheck: false,
|
|
4724
5083
|
onChange: function (event) { setValue(event.currentTarget.value); },
|
|
4725
|
-
onKeyDown: function (event) {
|
|
5084
|
+
onKeyDown: function (event) {
|
|
5085
|
+
if (event.key === 'Enter' && (!event.nativeEvent || !event.nativeEvent.isComposing)) { event.preventDefault(); submit(); }
|
|
5086
|
+
else if (event.key === 'Escape') close();
|
|
5087
|
+
},
|
|
4726
5088
|
}),
|
|
5089
|
+
status.error ? h(Banner, { kind: 'error' }, status.error) : null,
|
|
4727
5090
|
h('div', { className: 'rw-dialog-actions' },
|
|
4728
|
-
h('button', { type: 'button', className: 'rw-btn rw-btn-sm',
|
|
4729
|
-
h('button', { type: 'button', className: 'rw-btn rw-btn-sm rw-btn-primary', onClick: submit }, props.t('confirm'))));
|
|
5091
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm', disabled: status.busy, onClick: close }, props.t('cancel')),
|
|
5092
|
+
h('button', { type: 'button', className: 'rw-btn rw-btn-sm rw-btn-primary', disabled: status.busy || value.trim() === '', onClick: submit }, props.t(status.busy ? 'loading' : 'confirm'))));
|
|
4730
5093
|
}
|
|
4731
5094
|
|
|
4732
5095
|
function ConfirmDialog(props) {
|
|
@@ -5055,6 +5418,12 @@ function fileIconSpec(name, isDir) {
|
|
|
5055
5418
|
h('div', { className: 'rw-doc-tabs' }, sheets.map(function (sheet, index) {
|
|
5056
5419
|
return h('button', { key: sheet.name, type: 'button', className: 'rw-doc-tab' + (index === state.sheet ? ' is-on' : ''), onClick: function () { patch({ sheet: index }); } }, sheet.name);
|
|
5057
5420
|
})),
|
|
5421
|
+
active.truncated
|
|
5422
|
+
? h('div', { className: 'rw-doc-note' }, t('sheetTruncated', { rows: active.maxRow || 0, cols: active.maxCol || 0 }))
|
|
5423
|
+
: null,
|
|
5424
|
+
(state.data.sheetCount || sheets.length) > sheets.length
|
|
5425
|
+
? h('div', { className: 'rw-doc-note' }, t('sheetMore'))
|
|
5426
|
+
: null,
|
|
5058
5427
|
h('div', { style: { flex: '1 1 auto', overflow: 'auto', padding: 8 } },
|
|
5059
5428
|
h('table', { className: 'rw-doc-grid' },
|
|
5060
5429
|
h('thead', null, header),
|
|
@@ -5644,27 +6013,13 @@ function fileIconSpec(name, isDir) {
|
|
|
5644
6013
|
}
|
|
5645
6014
|
|
|
5646
6015
|
function submitNew(side, dirPath, name, isDir) {
|
|
5647
|
-
var
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
if (isDir) {
|
|
5651
|
-
promise = side === 'remote'
|
|
5652
|
-
? apiCall('fs.mkdir', Object.assign({ path: target }, scopeParams(scopeRef)))
|
|
5653
|
-
: apiCall('local.mkdir', { path: dirPath, name: name });
|
|
5654
|
-
} else if (docKind) {
|
|
5655
|
-
promise = side === 'remote'
|
|
5656
|
-
? apiCall('docs.create', Object.assign({ path: target, kind: docKind }, scopeParams(scopeRef)))
|
|
5657
|
-
: apiCall('docs.create', { path: target, kind: docKind });
|
|
5658
|
-
} else {
|
|
5659
|
-
promise = side === 'remote'
|
|
5660
|
-
? apiCall('fs.write', Object.assign({ path: target, content: '' }, scopeParams(scopeRef)))
|
|
5661
|
-
: apiCall('local.newFile', { path: target });
|
|
5662
|
-
}
|
|
5663
|
-
promise.then(function () {
|
|
6016
|
+
var request = newEntryRequest(side, dirPath, name, isDir, scopeParams(scopeRef));
|
|
6017
|
+
return apiCall(request.method, request.params).then(function () {
|
|
6018
|
+
setDialog(null);
|
|
5664
6019
|
showNotice('success', t('newDone'));
|
|
5665
6020
|
refreshTree();
|
|
5666
|
-
if (!isDir &&
|
|
5667
|
-
})
|
|
6021
|
+
if (!isDir && request.kind) openInternalFile(side, request.path);
|
|
6022
|
+
});
|
|
5668
6023
|
}
|
|
5669
6024
|
|
|
5670
6025
|
function submitChmod(side, targetPath, mode) {
|
|
@@ -5679,9 +6034,9 @@ function fileIconSpec(name, isDir) {
|
|
|
5679
6034
|
}
|
|
5680
6035
|
|
|
5681
6036
|
function submitDialog(current, value) {
|
|
6037
|
+
if (current.kind === 'new') return submitNew(current.side, current.dir, value, current.isDir);
|
|
5682
6038
|
setDialog(null);
|
|
5683
6039
|
if (current.kind === 'rename') submitRenamed(current.side, current.path, value);
|
|
5684
|
-
else if (current.kind === 'new') submitNew(current.side, current.dir, value, current.isDir);
|
|
5685
6040
|
else if (current.kind === 'chmod') submitChmod(current.side, current.path, value);
|
|
5686
6041
|
}
|
|
5687
6042
|
|
|
@@ -5741,14 +6096,8 @@ function fileIconSpec(name, isDir) {
|
|
|
5741
6096
|
setDialog({ kind: 'chmod', side: menu.side, path: menu.path, title: t('chmodTitle'), label: t('chmodLabel'), value: '644' });
|
|
5742
6097
|
} });
|
|
5743
6098
|
if (menu.isDir) {
|
|
5744
|
-
items.push({
|
|
5745
|
-
|
|
5746
|
-
setDialog({ kind: 'new', side: menu.side, dir: menu.path, isDir: true, title: t('newFolderTitle'), label: t('nameLabel'), value: t('newFolderTitle') });
|
|
5747
|
-
} },
|
|
5748
|
-
{ label: t('ctxNewFile'), onSelect: function () {
|
|
5749
|
-
setDialog({ kind: 'new', side: menu.side, dir: menu.path, isDir: false, title: t('newFileTitle'), label: t('nameLabel'), value: t('newFileTitle') });
|
|
5750
|
-
} },
|
|
5751
|
-
] });
|
|
6099
|
+
items.push({ separator: true });
|
|
6100
|
+
items = items.concat(newEntryMenuItems(menu, t, setDialog));
|
|
5752
6101
|
}
|
|
5753
6102
|
items.push({ separator: true });
|
|
5754
6103
|
items.push({ label: t('ctxProperties'), onSelect: function () { showProperties(menu.side, menu.path); } });
|
|
@@ -5809,11 +6158,12 @@ function fileIconSpec(name, isDir) {
|
|
|
5809
6158
|
}
|
|
5810
6159
|
return h(PromptDialog, {
|
|
5811
6160
|
t: t,
|
|
6161
|
+
locale: locale,
|
|
5812
6162
|
title: dialog.title,
|
|
5813
6163
|
label: dialog.label,
|
|
5814
6164
|
value: dialog.value,
|
|
5815
6165
|
onClose: function () { setDialog(null); },
|
|
5816
|
-
onSubmit: function (value) { submitDialog(dialog, value); },
|
|
6166
|
+
onSubmit: function (value) { return submitDialog(dialog, value); },
|
|
5817
6167
|
});
|
|
5818
6168
|
}
|
|
5819
6169
|
|
|
@@ -5959,6 +6309,13 @@ function fileIconSpec(name, isDir) {
|
|
|
5959
6309
|
path: activeFile.path,
|
|
5960
6310
|
local: activeFile.side === 'local',
|
|
5961
6311
|
})
|
|
6312
|
+
: activeDocKind === 'html'
|
|
6313
|
+
? h(HtmlPreview, {
|
|
6314
|
+
ctx: ctx, t: t, locale: locale,
|
|
6315
|
+
scope: activeFile.side === 'remote' ? sourceScopeOf(selected) : undefined,
|
|
6316
|
+
path: activeFile.path,
|
|
6317
|
+
local: activeFile.side === 'local',
|
|
6318
|
+
})
|
|
5962
6319
|
: activeDocKind && activeDocKind !== 'csv'
|
|
5963
6320
|
? h(DocumentPane, {
|
|
5964
6321
|
ctx: ctx, t: t, locale: locale,
|
|
@@ -6171,6 +6528,7 @@ function fileIconSpec(name, isDir) {
|
|
|
6171
6528
|
toggle: toggle,
|
|
6172
6529
|
state: state,
|
|
6173
6530
|
load: props.load,
|
|
6531
|
+
loadMore: props.loadMore,
|
|
6174
6532
|
onSelect: onSelect,
|
|
6175
6533
|
onPin: onPin,
|
|
6176
6534
|
onSelectPath: onSelectPath,
|
|
@@ -6182,6 +6540,9 @@ function fileIconSpec(name, isDir) {
|
|
|
6182
6540
|
children.push(h('button', {
|
|
6183
6541
|
key: child.path,
|
|
6184
6542
|
type: 'button',
|
|
6543
|
+
role: 'treeitem',
|
|
6544
|
+
'aria-level': depth + 1,
|
|
6545
|
+
'aria-selected': active,
|
|
6185
6546
|
className: 'rw-row' + (active ? ' is-active' : ''),
|
|
6186
6547
|
style: { paddingLeft: 6 + (depth + 1) * 12 },
|
|
6187
6548
|
title: child.path,
|
|
@@ -6203,12 +6564,28 @@ function fileIconSpec(name, isDir) {
|
|
|
6203
6564
|
h('span', { className: 'rw-row-size' }, formatBytes(child.size))));
|
|
6204
6565
|
}
|
|
6205
6566
|
});
|
|
6567
|
+
if (entry.hasMore) {
|
|
6568
|
+
children.push(h('button', {
|
|
6569
|
+
key: 'more',
|
|
6570
|
+
type: 'button',
|
|
6571
|
+
className: 'rw-row-msg rw-load-more',
|
|
6572
|
+
style: { paddingLeft: 6 + (depth + 1) * 12 },
|
|
6573
|
+
disabled: Boolean(entry.loadingMore),
|
|
6574
|
+
onClick: function () { if (props.loadMore) props.loadMore(path); },
|
|
6575
|
+
}, entry.loadingMore ? Spinner(12) : null,
|
|
6576
|
+
entry.moreError
|
|
6577
|
+
? messageOf(entry.moreError, t, locale)
|
|
6578
|
+
: t('loadMore', { count: Math.max((entry.total || 0) - entry.entries.length, 0) })));
|
|
6579
|
+
}
|
|
6206
6580
|
}
|
|
6207
6581
|
}
|
|
6208
6582
|
|
|
6209
6583
|
return h(React.Fragment, null,
|
|
6210
6584
|
h('button', {
|
|
6211
6585
|
type: 'button',
|
|
6586
|
+
role: 'treeitem',
|
|
6587
|
+
'aria-level': depth + 1,
|
|
6588
|
+
'aria-expanded': isOpen,
|
|
6212
6589
|
className: 'rw-row' + (selectedPath === path ? ' is-active' : ''),
|
|
6213
6590
|
style: { paddingLeft: 6 + depth * 12 },
|
|
6214
6591
|
title: path,
|
|
@@ -6216,13 +6593,16 @@ function fileIconSpec(name, isDir) {
|
|
|
6216
6593
|
if (onSelectPath) onSelectPath({ path: path, name: name, isDir: true });
|
|
6217
6594
|
if (event.detail !== 2) toggle(path);
|
|
6218
6595
|
},
|
|
6596
|
+
onKeyDown: function (event) {
|
|
6597
|
+
if (event.key === 'ArrowRight' && !isOpen) { event.preventDefault(); toggle(path); }
|
|
6598
|
+
else if (event.key === 'ArrowLeft' && isOpen) { event.preventDefault(); toggle(path); }
|
|
6599
|
+
},
|
|
6219
6600
|
onContextMenu: function (event) {
|
|
6220
6601
|
if (!onContextMenu) return;
|
|
6221
6602
|
event.preventDefault();
|
|
6222
6603
|
event.stopPropagation();
|
|
6223
6604
|
onContextMenu({ path: path, name: name, isDir: true, event: event });
|
|
6224
6605
|
},
|
|
6225
|
-
'aria-expanded': isOpen,
|
|
6226
6606
|
},
|
|
6227
6607
|
h('span', { className: 'rw-chevron' + (isOpen ? ' is-open' : '') }, IconChevron(12)),
|
|
6228
6608
|
h(FileGlyph, { name: name, dir: true, open: isOpen, size: 13 }),
|
|
@@ -6253,6 +6633,11 @@ function fileIconSpec(name, isDir) {
|
|
|
6253
6633
|
});
|
|
6254
6634
|
}
|
|
6255
6635
|
|
|
6636
|
+
function listRequest(path, offset) {
|
|
6637
|
+
return Object.assign({ path: path, limit: TREE_PAGE_SIZE, offset: offset },
|
|
6638
|
+
source.kind === 'linked' ? { linkedId: source.id } : { workspaceId: source.id });
|
|
6639
|
+
}
|
|
6640
|
+
|
|
6256
6641
|
function load(path) {
|
|
6257
6642
|
setCache(function (previous) {
|
|
6258
6643
|
var record = previous.entries.get(path);
|
|
@@ -6262,13 +6647,17 @@ function fileIconSpec(name, isDir) {
|
|
|
6262
6647
|
entries.set(path, { loading: true, error: null, entries: existing.entries || [], loaded: existing.loaded });
|
|
6263
6648
|
return { entries: entries };
|
|
6264
6649
|
});
|
|
6265
|
-
|
|
6266
|
-
? { linkedId: source.id, path: path }
|
|
6267
|
-
: { workspaceId: source.id, path: path };
|
|
6268
|
-
apiCall('fs.list', request).then(function (result) {
|
|
6650
|
+
apiCall('fs.list', listRequest(path, 0)).then(function (result) {
|
|
6269
6651
|
setCache(function (previous) {
|
|
6270
6652
|
var entries = new Map(previous.entries);
|
|
6271
|
-
entries.set(path, {
|
|
6653
|
+
entries.set(path, {
|
|
6654
|
+
loading: false,
|
|
6655
|
+
error: null,
|
|
6656
|
+
loaded: true,
|
|
6657
|
+
entries: result.entries || [],
|
|
6658
|
+
total: Number(result.total) || (result.entries || []).length,
|
|
6659
|
+
hasMore: Boolean(result.hasMore),
|
|
6660
|
+
});
|
|
6272
6661
|
return { entries: entries };
|
|
6273
6662
|
});
|
|
6274
6663
|
setExpanded(function (previous) {
|
|
@@ -6285,6 +6674,38 @@ function fileIconSpec(name, isDir) {
|
|
|
6285
6674
|
});
|
|
6286
6675
|
}
|
|
6287
6676
|
|
|
6677
|
+
function loadMore(path) {
|
|
6678
|
+
var record = cache.entries.get(path);
|
|
6679
|
+
if (!record || record.loadingMore || !record.hasMore) return;
|
|
6680
|
+
setCache(function (previous) {
|
|
6681
|
+
var entries = new Map(previous.entries);
|
|
6682
|
+
var current = entries.get(path);
|
|
6683
|
+
if (current) entries.set(path, Object.assign({}, current, { loadingMore: true }));
|
|
6684
|
+
return { entries: entries };
|
|
6685
|
+
});
|
|
6686
|
+
apiCall('fs.list', listRequest(path, (record.entries || []).length)).then(function (result) {
|
|
6687
|
+
setCache(function (previous) {
|
|
6688
|
+
var entries = new Map(previous.entries);
|
|
6689
|
+
var current = entries.get(path) || { entries: [] };
|
|
6690
|
+
entries.set(path, Object.assign({}, current, {
|
|
6691
|
+
loadingMore: false,
|
|
6692
|
+
loaded: true,
|
|
6693
|
+
entries: current.entries.concat(result.entries || []),
|
|
6694
|
+
total: Number(result.total) || current.total,
|
|
6695
|
+
hasMore: Boolean(result.hasMore),
|
|
6696
|
+
}));
|
|
6697
|
+
return { entries: entries };
|
|
6698
|
+
});
|
|
6699
|
+
}).catch(function (error) {
|
|
6700
|
+
setCache(function (previous) {
|
|
6701
|
+
var entries = new Map(previous.entries);
|
|
6702
|
+
var current = entries.get(path);
|
|
6703
|
+
if (current) entries.set(path, Object.assign({}, current, { loadingMore: false, moreError: error }));
|
|
6704
|
+
return { entries: entries };
|
|
6705
|
+
});
|
|
6706
|
+
});
|
|
6707
|
+
}
|
|
6708
|
+
|
|
6288
6709
|
useEffect(function () {
|
|
6289
6710
|
if (!open) return;
|
|
6290
6711
|
var root = source.remotePath;
|
|
@@ -6313,7 +6734,7 @@ function fileIconSpec(name, isDir) {
|
|
|
6313
6734
|
h('span', { className: 'rw-tree-ws-sub' }, source.username + '@' + source.host + (source.kind === 'linked' ? ' · ' + t('linkedChip') : ''))),
|
|
6314
6735
|
h(StatusPill, { status: source.status, t: t })),
|
|
6315
6736
|
open
|
|
6316
|
-
? h('div', { className: 'rw-tree-body' },
|
|
6737
|
+
? h('div', { className: 'rw-tree-body', role: 'tree', 'aria-label': source.name },
|
|
6317
6738
|
h(TreeDirectory, {
|
|
6318
6739
|
path: source.remotePath,
|
|
6319
6740
|
name: '/',
|
|
@@ -6325,6 +6746,7 @@ function fileIconSpec(name, isDir) {
|
|
|
6325
6746
|
toggle: toggle,
|
|
6326
6747
|
state: { entries: cache.entries },
|
|
6327
6748
|
load: load,
|
|
6749
|
+
loadMore: loadMore,
|
|
6328
6750
|
onSelect: props.onSelect,
|
|
6329
6751
|
selectedPath: props.selectedPath,
|
|
6330
6752
|
}))
|
|
@@ -6601,10 +7023,58 @@ function fileIconSpec(name, isDir) {
|
|
|
6601
7023
|
});
|
|
6602
7024
|
}
|
|
6603
7025
|
|
|
7026
|
+
var modalRef = useRef(null);
|
|
7027
|
+
var focusablesOf = function () {
|
|
7028
|
+
var node = modalRef.current;
|
|
7029
|
+
if (!node) return [];
|
|
7030
|
+
return Array.prototype.filter.call(
|
|
7031
|
+
node.querySelectorAll('button, input, select, textarea, a[href], [tabindex]:not([tabindex="-1"])'),
|
|
7032
|
+
function (element) { return !element.disabled && element.offsetParent !== null; });
|
|
7033
|
+
};
|
|
7034
|
+
|
|
7035
|
+
useEffect(function () {
|
|
7036
|
+
if (!props.open) return;
|
|
7037
|
+
var node = modalRef.current;
|
|
7038
|
+
if (!node) return;
|
|
7039
|
+
// Focus the first real field, not the tab strip, so typing works at once.
|
|
7040
|
+
var target = node.querySelector('.rw-modal-body input, .rw-modal-body textarea') || focusablesOf()[0];
|
|
7041
|
+
if (target && typeof target.focus === 'function') target.focus();
|
|
7042
|
+
}, [props.open, flow]);
|
|
7043
|
+
|
|
7044
|
+
function onModalKeyDown(event) {
|
|
7045
|
+
if (event.key === 'Escape') {
|
|
7046
|
+
event.preventDefault();
|
|
7047
|
+
event.stopPropagation();
|
|
7048
|
+
if (props.onCancel) props.onCancel();
|
|
7049
|
+
return;
|
|
7050
|
+
}
|
|
7051
|
+
if (event.key !== 'Tab') return;
|
|
7052
|
+
// Keep Tab inside the dialog: without this the focus escapes to the
|
|
7053
|
+
// page behind the backdrop.
|
|
7054
|
+
var focusables = focusablesOf();
|
|
7055
|
+
if (focusables.length === 0) return;
|
|
7056
|
+
var first = focusables[0];
|
|
7057
|
+
var last = focusables[focusables.length - 1];
|
|
7058
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
7059
|
+
event.preventDefault();
|
|
7060
|
+
last.focus();
|
|
7061
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
7062
|
+
event.preventDefault();
|
|
7063
|
+
first.focus();
|
|
7064
|
+
}
|
|
7065
|
+
}
|
|
7066
|
+
|
|
6604
7067
|
if (!props.open) return null;
|
|
6605
7068
|
|
|
6606
7069
|
return h('div', { className: 'rw-modal-backdrop', role: 'presentation' },
|
|
6607
|
-
h('div', {
|
|
7070
|
+
h('div', {
|
|
7071
|
+
ref: modalRef,
|
|
7072
|
+
className: 'rw-modal',
|
|
7073
|
+
role: 'dialog',
|
|
7074
|
+
'aria-modal': 'true',
|
|
7075
|
+
'aria-label': t('flowTitle'),
|
|
7076
|
+
onKeyDown: onModalKeyDown,
|
|
7077
|
+
},
|
|
6608
7078
|
h('div', { className: 'rw-modal-head' },
|
|
6609
7079
|
h('span', { className: 'rw-icon-chip' }, IconLink(16)),
|
|
6610
7080
|
h('span', { className: 'rw-modal-title' }, t('flowTitle')),
|