Skip to content

Commit bbbde0a

Browse files
committed
feat(docs): add document permission controls
1 parent 1d4112c commit bbbde0a

23 files changed

Lines changed: 2009 additions & 57 deletions

packages/docs-drawing-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@
8787
"devDependencies": {
8888
"@testing-library/react": "^16.3.2",
8989
"@univerjs-infra/shared": "workspace:*",
90+
"@univerjs/protocol": "workspace:*",
9091
"postcss": "^8.5.25",
9192
"react": "18.3.1",
9293
"rxjs": "^7.8.2",

packages/docs-drawing-ui/src/controllers/render-controllers/__tests__/doc-drawing-update.render-controller.spec.ts

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,11 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { BooleanNumber, DrawingTypeEnum, FOCUSING_COMMON_DRAWINGS, ObjectRelativeFromH, ObjectRelativeFromV, PositionedObjectLayoutType } from '@univerjs/core';
18-
import { RichTextEditingMutation } from '@univerjs/docs';
17+
import { BooleanNumber, DrawingTypeEnum, FOCUSING_COMMON_DRAWINGS, ObjectRelativeFromH, ObjectRelativeFromV, PermissionService, PositionedObjectLayoutType } from '@univerjs/core';
18+
import { RichTextEditingMutation, setDocumentPermissionValue } from '@univerjs/docs';
1919
import { SetDocDrawingArrangeCommand, UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing';
2020
import { DocumentEditArea } from '@univerjs/engine-render';
21+
import { UnitAction } from '@univerjs/protocol';
2122
import { Subject } from 'rxjs';
2223
import { describe, expect, it, vi } from 'vitest';
2324
import { GroupDocDrawingCommand } from '../../../commands/commands/group-doc-drawing.command';
@@ -36,6 +37,8 @@ function createController(options: {
3637
const featurePluginGroupUpdate$ = new Subject<any>();
3738
const featurePluginUngroupUpdate$ = new Subject<any>();
3839
const focus$ = new Subject<any[] | null>();
40+
const add$ = new Subject<Array<{ unitId: string }>>();
41+
const update$ = new Subject<Array<{ unitId: string }>>();
3942
const changeEnd$ = new Subject<any>();
4043
const editAreaChange$ = new Subject<void>();
4144
const refreshDrawings$ = new Subject<unknown>();
@@ -47,6 +50,7 @@ function createController(options: {
4750

4851
const transformer = {
4952
changeEnd$,
53+
clearControlByIds: vi.fn(),
5054
resetProps: vi.fn(),
5155
};
5256
const shapeByDrawingId = new Map<string, any>();
@@ -57,13 +61,17 @@ function createController(options: {
5761
if (!shapeByDrawingId.has(drawingId)) {
5862
shapeByDrawingId.set(drawingId, {
5963
drawingId,
64+
evented: true,
65+
oKey: drawingId,
6066
setOpacity: vi.fn(),
67+
transformerConfig: {},
6168
});
6269
}
6370
return [shapeByDrawingId.get(drawingId)];
6471
}),
6572
attachTransformerTo: vi.fn(),
6673
detachTransformerFrom: vi.fn(),
74+
getTransformer: vi.fn(() => transformer),
6775
};
6876
const viewModel = {
6977
editAreaChange$,
@@ -76,13 +84,15 @@ function createController(options: {
7684
};
7785
const snapshot = {
7886
body: {
87+
dataStream: 'abcdefghij\r\n',
7988
customBlocks: [
8089
{ blockId: 'body-drawing', startIndex: 4 },
8190
],
8291
},
8392
headers: {
8493
'header-1': {
8594
body: {
95+
dataStream: 'abcdefghij\r\n',
8696
customBlocks: [
8797
{ blockId: 'header-drawing', startIndex: 2 },
8898
],
@@ -92,6 +102,7 @@ function createController(options: {
92102
footers: {
93103
'footer-1': {
94104
body: {
105+
dataStream: 'abcdefghij\r\n',
95106
customBlocks: [
96107
{ blockId: 'footer-drawing', startIndex: 7 },
97108
],
@@ -105,6 +116,10 @@ function createController(options: {
105116
unit: {
106117
getDrawings: vi.fn(() => snapshot.drawings),
107118
getSnapshot: vi.fn(() => snapshot),
119+
getBody: vi.fn(() => snapshot.body),
120+
getSelfOrHeaderFooterModel: vi.fn((segmentId: string) => ({
121+
getBody: () => segmentId ? snapshot.headers[segmentId as keyof typeof snapshot.headers]?.body ?? snapshot.footers[segmentId as keyof typeof snapshot.footers]?.body : snapshot.body,
122+
})),
108123
},
109124
scene,
110125
mainComponent: {
@@ -131,12 +146,16 @@ function createController(options: {
131146
getDrawingByParam: vi.fn(({ drawingId }: { drawingId: string }) => options.drawings?.[drawingId]),
132147
};
133148
const drawingManagerService = {
149+
add$,
134150
featurePluginUpdate$,
135151
featurePluginOrderUpdate$,
136152
featurePluginGroupUpdate$,
137153
featurePluginUngroupUpdate$,
138154
focus$,
155+
update$,
139156
getDrawingByParam: vi.fn(({ drawingId }: { drawingId: string }) => options.drawings?.[drawingId]),
157+
focusDrawing: vi.fn(),
158+
getFocusDrawings: vi.fn(() => []),
140159
};
141160
const contextService = {
142161
setContextValue: vi.fn(),
@@ -160,6 +179,7 @@ function createController(options: {
160179
openFile: vi.fn(options.openFile ?? (async () => [])),
161180
};
162181

182+
const permissionService = new PermissionService();
163183
const controller = new DocDrawingUpdateRenderController(
164184
context as never,
165185
commandService as never,
@@ -168,6 +188,7 @@ function createController(options: {
168188
imageIoService as never,
169189
docDrawingService as never,
170190
drawingManagerService as never,
191+
permissionService,
171192
contextService as never,
172193
{ show: vi.fn() } as never,
173194
{ t: vi.fn((key: string) => key) } as never,
@@ -191,6 +212,7 @@ function createController(options: {
191212
getShape: (drawingId: string) => shapeByDrawingId.get(drawingId),
192213
onBlur$,
193214
onFocus$,
215+
permissionService,
194216
refreshDrawings$,
195217
scene,
196218
setEditArea: (value: DocumentEditArea) => {
@@ -200,6 +222,7 @@ function createController(options: {
200222
isFocusing = value;
201223
},
202224
transformer,
225+
update$,
203226
};
204227
}
205228

@@ -394,6 +417,41 @@ describe('DocDrawingUpdateRenderController', () => {
394417
expect(commandService.executeCommand).not.toHaveBeenCalled();
395418
});
396419

420+
it('does not open the image picker when Document Edit is denied', async () => {
421+
const openFile = vi.fn(async () => [{} as File]);
422+
const { commandService, controller, permissionService } = createController({ openFile });
423+
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, false);
424+
425+
await expect(controller.insertDocImage()).resolves.toBe(false);
426+
427+
expect(openFile).not.toHaveBeenCalled();
428+
expect(commandService.executeCommand).not.toHaveBeenCalled();
429+
});
430+
431+
it('does not insert an image when Document Edit is revoked during upload', async () => {
432+
let finishSaving: (value: unknown) => void = () => {};
433+
const saveImage = vi.fn(() => new Promise<unknown>((resolve) => {
434+
finishSaving = resolve;
435+
}));
436+
const { commandService, controller, permissionService } = createController({
437+
openFile: async () => [{} as File],
438+
saveImage,
439+
});
440+
441+
const insertion = controller.insertDocImage();
442+
await vi.waitFor(() => expect(saveImage).toHaveBeenCalledTimes(1));
443+
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, false);
444+
finishSaving({
445+
imageId: 'image-1',
446+
imageSourceType: 'URL',
447+
source: 'image.png',
448+
base64Cache: 'data:image/png;base64,',
449+
});
450+
451+
await expect(insertion).resolves.toBe(false);
452+
expect(commandService.executeCommand).not.toHaveBeenCalled();
453+
});
454+
397455
it('toggles drawing editability between body and header/footer edit areas', () => {
398456
const bodyDrawing = {
399457
drawingId: 'body-drawing',
@@ -423,6 +481,53 @@ describe('DocDrawingUpdateRenderController', () => {
423481
expect(getShape('body-drawing').setOpacity).toHaveBeenLastCalledWith(0.5);
424482
});
425483

484+
it('removes read-only drawings from picking and transformer interaction, then restores both', async () => {
485+
const { getShape, permissionService, scene, transformer } = createController({
486+
drawings: {
487+
'body-drawing': {
488+
drawingId: 'body-drawing',
489+
isMultiTransform: BooleanNumber.FALSE,
490+
},
491+
},
492+
});
493+
494+
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, false);
495+
496+
expect(getShape('body-drawing').evented).toBe(false);
497+
expect(transformer.clearControlByIds).toHaveBeenCalledWith(['body-drawing']);
498+
expect(getShape('body-drawing').transformerConfig).toMatchObject({
499+
moveEnabled: false,
500+
resizeEnabled: false,
501+
rotateEnabled: false,
502+
});
503+
504+
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, true);
505+
expect(getShape('body-drawing').evented).toBe(true);
506+
expect(scene.attachTransformerTo).toHaveBeenLastCalledWith(getShape('body-drawing'));
507+
expect(getShape('body-drawing').transformerConfig).not.toHaveProperty('moveEnabled', false);
508+
expect(getShape('body-drawing').transformerConfig).not.toHaveProperty('resizeEnabled', false);
509+
expect(getShape('body-drawing').transformerConfig).not.toHaveProperty('rotateEnabled', false);
510+
});
511+
512+
it('keeps a remotely updated drawing non-interactive while local permission is read-only', async () => {
513+
const { getShape, permissionService, scene, update$ } = createController({
514+
drawings: {
515+
'body-drawing': {
516+
drawingId: 'body-drawing',
517+
isMultiTransform: BooleanNumber.FALSE,
518+
},
519+
},
520+
});
521+
setDocumentPermissionValue(permissionService, 'doc-1', 'doc-1', UnitAction.Edit, false);
522+
scene.attachTransformerTo.mockClear();
523+
524+
update$.next([{ unitId: 'doc-1' }]);
525+
await Promise.resolve();
526+
527+
expect(getShape('body-drawing').evented).toBe(false);
528+
expect(scene.attachTransformerTo).not.toHaveBeenCalled();
529+
});
530+
426531
it('keeps all drawings opaque while the document input is not focused', () => {
427532
const bodyDrawing = {
428533
drawingId: 'body-drawing',

0 commit comments

Comments
 (0)