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' ;
1919import { SetDocDrawingArrangeCommand , UpdateDrawingDocTransformCommand } from '@univerjs/docs-drawing' ;
2020import { DocumentEditArea } from '@univerjs/engine-render' ;
21+ import { UnitAction } from '@univerjs/protocol' ;
2122import { Subject } from 'rxjs' ;
2223import { describe , expect , it , vi } from 'vitest' ;
2324import { 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