Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

host: Add read-only state for code mode editor #1039

Merged
merged 13 commits into from
Feb 20, 2024
5 changes: 5 additions & 0 deletions packages/host/app/components/operator-mode/code-editor.gts
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,10 @@ export default class CodeEditor extends Component<Signature> {
return undefined;
}

private get readOnly() {
return !this.readyFile.realmSession.canWrite;
}

<template>
{{#if this.isReady}}
{{#if this.readyFile.isBinary}}
Expand All @@ -303,6 +307,7 @@ export default class CodeEditor extends Component<Signature> {
language=this.language
initialCursorPosition=this.initialMonacoCursorPosition
onCursorPositionChange=this.selectDeclarationByMonacoCursorPosition
readOnly=this.readOnly
}}
></div>
{{/if}}
Expand Down
3 changes: 3 additions & 0 deletions packages/host/app/modifiers/monaco.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface Signature {
onCursorPositionChange?: (position: MonacoSDK.Position) => void;
onSetup?: (editor: MonacoSDK.editor.IStandaloneCodeEditor) => void;
language?: string;
readOnly?: boolean;
monacoSDK: typeof MonacoSDK;
};
};
Expand All @@ -46,6 +47,7 @@ export default class Monaco extends Modifier<Signature> {
initialCursorPosition,
onCursorPositionChange,
onSetup,
readOnly,
monacoSDK,
}: Signature['Args']['Named'],
) {
Expand All @@ -66,6 +68,7 @@ export default class Monaco extends Modifier<Signature> {
} else {
let editorOptions: MonacoSDK.editor.IStandaloneEditorConstructionOptions =
{
readOnly,
value: content,
language,
scrollBeyondLastLine: false,
Expand Down
25 changes: 25 additions & 0 deletions packages/host/app/styles/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,28 @@
.link:hover {
color: var(--boxel-highlight-hover);
}

/* Style Monaco read-only tooltip, copied from packages/boxel-ui/addon/src/components/tooltip/index.gts */
.ember-application .monaco-editor .monaco-editor-overlaymessage {
background-color: rgb(0 0 0 / 80%);
box-shadow: 0 0 0 1px var(--boxel-light-500);
color: var(--boxel-light);
text-align: center;
border-radius: var(--boxel-border-radius-sm);
padding: var(--boxel-sp-xxxs) var(--boxel-sp-sm);
width: max-content;
position: absolute;
font: var(--boxel-tooltip-font, var(--boxel-font-xs));
z-index: 5;

.message {
padding: 0;
color: inherit;
background-color: transparent;
border: 0;
}

.anchor.top, .anchor.below {
display: none;
}
}
50 changes: 49 additions & 1 deletion packages/host/tests/acceptance/code-submode/editor-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { setupApplicationTest } from 'ember-qunit';

import window from 'ember-window-mock';
import { setupWindowMock } from 'ember-window-mock/test-support';
import * as MonacoSDK from 'monaco-editor';
import { module, test } from 'qunit';
import stringify from 'safe-stable-stringify';

Expand All @@ -15,6 +16,7 @@ import { Realm } from '@cardstack/runtime-common/realm';

import type EnvironmentService from '@cardstack/host/services/environment-service';
import type LoaderService from '@cardstack/host/services/loader-service';
import type MonacoService from '@cardstack/host/services/monaco-service';

import {
TestRealmAdapter,
Expand All @@ -34,23 +36,34 @@ import {
} from '../../helpers';
import { setupMatrixServiceMock } from '../../helpers/mock-matrix-service';

let realmPermissions: { [realmURL: string]: ('read' | 'write')[] } = {
[testRealmURL]: ['read', 'write'],
};

module('Acceptance | code submode | editor tests', function (hooks) {
let realm: Realm;
let monacoService: MonacoService;
let adapter: TestRealmAdapter;

setupApplicationTest(hooks);
setupLocalIndexing(hooks);
setupServerSentEvents(hooks);
setupOnSave(hooks);
setupWindowMock(hooks);
setupMatrixServiceMock(hooks);
setupMatrixServiceMock(hooks, () => realmPermissions);

hooks.afterEach(async function () {
window.localStorage.removeItem('recent-cards');
window.localStorage.removeItem('recent-files');
});

hooks.beforeEach(async function () {
realmPermissions = { [testRealmURL]: ['read', 'write'] };

monacoService = this.owner.lookup(
'service:monaco-service',
) as MonacoService;

window.localStorage.removeItem('recent-cards');
window.localStorage.removeItem('recent-files');

Expand Down Expand Up @@ -307,6 +320,11 @@ module('Acceptance | code submode | editor tests', function (hooks) {
codePath: `${testRealmURL}Pet/mango.json`,
});
await waitForCodeEditor();

assert.false(
monacoService?.editor?.getOption(MonacoSDK.editor.EditorOption.readOnly),
'editor should not be read-only',
);
assert.deepEqual(JSON.parse(getMonacoContent()), {
data: {
attributes: {
Expand Down Expand Up @@ -755,4 +773,34 @@ module('Acceptance | code submode | editor tests', function (hooks) {
'pet.gts changes were saved',
);
});

module('when the user lacks write permissions', function (hooks) {
hooks.beforeEach(function () {
realmPermissions = { [testRealmURL]: ['read'] };
});

test('the editor is read-only', async function (assert) {
await visitOperatorMode({
stacks: [
[
{
id: `${testRealmURL}Pet/mango`,
format: 'isolated',
},
],
],
submode: 'code',
codePath: `${testRealmURL}Pet/mango.json`,
});

await waitForCodeEditor();

assert.true(
monacoService?.editor?.getOption(
MonacoSDK.editor.EditorOption.readOnly,
),
'editor should be read-only',
);
});
});
});
Loading