;
+}
type DraftState = {
globals: {
- [key: string]: DraftStateItem;
+ [key: string]: DraftStateGlobalItem;
};
config: {
[key: string]: DraftStateItem;
@@ -186,6 +191,7 @@ export class SessionState extends Model({
.filter(
(prop) =>
prop.name !== "id" &&
+ prop.cardinality !== "Many" &&
!prop.annotations.some(
(anno) =>
(anno.name === "cfg::system" || anno.name === "cfg::internal") &&
@@ -221,7 +227,8 @@ export class SessionState extends Model({
}
for (const configName of this.configNames) {
- const type = configType.properties[configName].target!;
+ const configSchema = configType.properties[configName];
+ const type = configSchema.target!;
const storedItem = sessionStateData?.config[configName];
const newVal =
storedItem?.value == null
@@ -229,7 +236,7 @@ export class SessionState extends Model({
: null;
draftState.config[configName] = {
type: frozen(type, FrozenCheckMode.Off),
- description: configType.properties[configName].annotations.find(
+ description: configSchema.annotations.find(
(anno) => anno.name === "std::description"
)?.["@value"],
active: storedItem?.active ?? false,
@@ -337,18 +344,25 @@ export class SessionState extends Model({
const result = await conn.query(
`select cfg::Config {${this.configNames.join(", ")}}`,
undefined,
- {ignoreSessionConfig: true}
+ {ignoreSessionConfig: true, ignoreForceDatabaseError: true}
);
if (result.result) {
const values = result.result![0];
runInAction(() => (this.configValues = values));
for (const configName of this.configNames) {
- if (this.draftState?.config[configName].value === null) {
+ const config = this.draftState?.config[configName];
+ if (config?.active === false) {
+ let value = values[configName];
+ if (typeof value === "boolean") {
+ value = !value;
+ }
objectActions.set(
- this.draftState.config[configName],
+ this.draftState!.config[configName],
"value",
- values[configName].toString()
+ frozen(
+ valueToEditorValue(value, config.type.data as PrimitiveType)
+ )
);
}
}
diff --git a/shared/studio/tabs/dataview/dataInspector.tsx b/shared/studio/tabs/dataview/dataInspector.tsx
index 3b9e3f12..11a9ce36 100644
--- a/shared/studio/tabs/dataview/dataInspector.tsx
+++ b/shared/studio/tabs/dataview/dataInspector.tsx
@@ -391,12 +391,8 @@ const GridCell = observer(function GridCell({
field.type === ObjectFieldType.property &&
edits.activePropertyEdit?.cellId === cellId
) {
- return cellEditState?.value === undefined &&
- field.schemaType.name === "std::str" &&
- typeof value === "string" &&
- value.length === 100 &&
- !state.fullyFetchedData.has(cellId) ? (
-
+ return !edits.activePropertyEdit.loaded ? (
+ loading...
) : (
);
@@ -532,14 +528,15 @@ const GridCell = observer(function GridCell({
[styles.hasEdits]:
!isDeletedRow && rowData && (!!cellEditState || !!linkEditState),
[styles.hasErrors]:
- (cellEditState && !cellEditState.value.valid) ||
- (insertedRow && _value && !_value.valid) ||
- (!rowData &&
- isEditable &&
- field.required &&
- !field.hasDefault &&
- value === null &&
- !linkEditState),
+ isEditable &&
+ ((cellEditState && !cellEditState.value.valid) ||
+ (insertedRow && _value && !_value.valid) ||
+ (!rowData &&
+ isEditable &&
+ field.required &&
+ !field.hasDefault &&
+ value === null &&
+ !linkEditState)),
})}
onClick={() => {
if (field.type === ObjectFieldType.link && content !== null) {
@@ -548,18 +545,16 @@ const GridCell = observer(function GridCell({
navigate,
data.id,
data.__tname__,
- field
+ field,
+ typeof data.id === "number"
);
}
}}
onDoubleClick={() => {
if (isEditable) {
if (field.type === ObjectFieldType.property) {
- edits.startEditingCell(
- data.id,
- data.__tname__,
- field,
- state.fullyFetchedData.get(cellId) ?? value
+ edits.startEditingCell(data.id, data.__tname__, field, () =>
+ state.fetchFullCellData(data.id, value, field)
);
} else {
state.openNestedView(
@@ -579,22 +574,6 @@ const GridCell = observer(function GridCell({
);
});
-function FetchingDataPlaceholder({
- state,
- data,
- field,
-}: {
- state: DataInspectorState;
- data: any;
- field: ObjectField;
-}) {
- useEffect(() => {
- state.fetchFullCellData(data.id, field);
- }, []);
-
- return loading...
;
-}
-
const FieldHeaders = observer(function FieldHeaders() {
const {state} = useDataInspectorState();
const isMobile = useIsMobile();
diff --git a/shared/studio/tabs/dataview/index.tsx b/shared/studio/tabs/dataview/index.tsx
index 20e53d7f..afe376a2 100644
--- a/shared/studio/tabs/dataview/index.tsx
+++ b/shared/studio/tabs/dataview/index.tsx
@@ -181,7 +181,10 @@ const DataInspectorView = observer(function DataInspectorView({
{!isMobile && (
{inspectorState.rowCount !== null ? (
- <>{inspectorState.rowCount} Items>
+ <>
+ {inspectorState.rowCount} Item
+ {inspectorState.rowCount === 1 ? "" : "s"}
+ >
) : (
loading...
)}
@@ -225,7 +228,8 @@ const DataInspectorView = observer(function DataInspectorView({
items={null}
actions={inspectorState.insertTypeNames.map((name) => ({
label: name,
- action: () => dataviewState.edits.createNewRow(name),
+ action: () =>
+ dataviewState.edits.createNewRow(name, inspectorState),
}))}
rightAlign
/>
@@ -234,7 +238,8 @@ const DataInspectorView = observer(function DataInspectorView({
className={styles.headerButton}
onClick={() =>
dataviewState.edits.createNewRow(
- inspectorState.insertTypeNames[0]
+ inspectorState.insertTypeNames[0],
+ inspectorState
)
}
>
diff --git a/shared/studio/tabs/dataview/reviewEditsModal.tsx b/shared/studio/tabs/dataview/reviewEditsModal.tsx
index 6a3e014e..ecdf4be2 100644
--- a/shared/studio/tabs/dataview/reviewEditsModal.tsx
+++ b/shared/studio/tabs/dataview/reviewEditsModal.tsx
@@ -191,7 +191,10 @@ function renderParam({
type.schemaType === "Range" || type.schemaType === "Multirange"
? type.elementType.name
: undefined,
- false
+ false,
+ undefined,
+ undefined,
+ 50
).body;
}
}
diff --git a/shared/studio/tabs/dataview/state/edits.ts b/shared/studio/tabs/dataview/state/edits.ts
index a97b466e..c47a06a3 100644
--- a/shared/studio/tabs/dataview/state/edits.ts
+++ b/shared/studio/tabs/dataview/state/edits.ts
@@ -14,7 +14,7 @@ import {
import {PrimitiveType} from "../../../components/dataEditor";
import {connCtx, dbCtx} from "../../../state";
-import {ObjectPropertyField} from ".";
+import {ObjectPropertyField, DataInspector} from ".";
enum EditKind {
UpdateProperty,
@@ -106,7 +106,7 @@ export class DataEditingManager extends Model({}) {
objectId: string | number,
objectTypeName: string,
field: ObjectPropertyField,
- value: any
+ valueGetter: () => any
) {
const cellId = `${objectId}__${field.name}`;
@@ -117,7 +117,7 @@ export class DataEditingManager extends Model({}) {
field.schemaType as PrimitiveType,
field.required,
field.multi,
- editValue?.value ?? value,
+ editValue ? editValue.value : valueGetter(),
editValue != null && !editValue.valid,
(discard) => {
if (!discard && state.isEdited) {
@@ -179,7 +179,7 @@ export class DataEditingManager extends Model({}) {
}
@action
- createNewRow(objectTypeName: string) {
+ createNewRow(objectTypeName: string, dataInspState: DataInspector) {
const insertId = insertEditId++;
this.insertEdits.set(insertId, {
kind: EditKind.InsertObject,
@@ -191,6 +191,18 @@ export class DataEditingManager extends Model({}) {
__tname__: objectTypeName,
},
});
+
+ const parentObject = dataInspState.parentObject;
+ if (parentObject?.editMode) {
+ this.toggleLinkInsert(
+ parentObject.id,
+ parentObject.subtypeName ?? parentObject.objectTypeName,
+ parentObject.fieldName,
+ dataInspState.objectType!,
+ this.insertEdits.get(insertId)!,
+ !parentObject.isMultiLink
+ );
+ }
}
@action
diff --git a/shared/studio/tabs/dataview/state/index.ts b/shared/studio/tabs/dataview/state/index.ts
index adcdcef6..f8377e61 100644
--- a/shared/studio/tabs/dataview/state/index.ts
+++ b/shared/studio/tabs/dataview/state/index.ts
@@ -496,7 +496,9 @@ export class DataInspector extends Model({
});
this.allFields = {
- fields: [...baseFields, ...subtypeFields],
+ fields: [...baseFields, ...subtypeFields].sort((a, b) =>
+ a.name === "id" ? -1 : b.name === "id" ? 1 : 0
+ ),
subtypeIndex: baseFields.length,
};
this.fieldWidths = Array(this.allFields.fields.length).fill(180);
@@ -788,16 +790,44 @@ export class DataInspector extends Model({
@observable.shallow
fullyFetchedData = new Map();
- async fetchFullCellData(dataId: string, field: ObjectField) {
+ fetchFullCellData(
+ dataId: string | number,
+ value: any,
+ field: ObjectPropertyField
+ ) {
+ if (typeof dataId !== "string") {
+ return value;
+ }
+
+ const cellId = `${dataId}__${field.name}`;
+
+ if (this.fullyFetchedData.has(cellId)) {
+ return this.fullyFetchedData.get(cellId);
+ }
+
+ const isTruncated =
+ field.schemaType.name === "std::str" &&
+ (field.multi
+ ? Array.isArray(value) &&
+ typeof value[0] === "string" &&
+ value.some((val) => val.length === 100)
+ : typeof value === "string" && value.length === 100);
+
+ if (!isTruncated) {
+ return value;
+ }
+
const query = `select (select ${
field.escapedSubtypeName ?? this._getObjectTypeQuery
} filter .id = $id).${field.escapedName}`;
- const conn = connCtx.get(this)!;
- const val = (await conn.query(query, {id: dataId})).result![0];
-
- runInAction(() => {
- this.fullyFetchedData.set(`${dataId}__${field.name}`, val);
+ const conn = connCtx.get(this)!;
+ return conn.query(query, {id: dataId}).then(({result}) => {
+ const val = field.multi ? result! : result![0];
+ runInAction(() => {
+ this.fullyFetchedData.set(cellId, val);
+ });
+ return val;
});
}
@@ -1143,7 +1173,8 @@ export class DataInspector extends Model({
.replace(/#.*/g, "")
.trimStart()
.replace(/^filter\s/i, "")
- .trimEnd(),
+ .trimEnd()
+ .replace(/;+$/, ""),
filter,
] as [string, string];
}
diff --git a/shared/studio/tabs/repl/index.tsx b/shared/studio/tabs/repl/index.tsx
index fd2bd816..5c9abc10 100644
--- a/shared/studio/tabs/repl/index.tsx
+++ b/shared/studio/tabs/repl/index.tsx
@@ -325,14 +325,14 @@ const ReplInput = observer(function ReplInput() {
{
key: "Mod-ArrowUp",
run: () => {
- replState.navigateHistory(-1);
+ replState.navigateHistory(1);
return true;
},
},
{
key: "Mod-ArrowDown",
run: () => {
- replState.navigateHistory(1);
+ replState.navigateHistory(-1);
return true;
},
},
diff --git a/shared/studio/tabs/repl/state/index.ts b/shared/studio/tabs/repl/state/index.ts
index 9b122ef8..bfe9f4e6 100644
--- a/shared/studio/tabs/repl/state/index.ts
+++ b/shared/studio/tabs/repl/state/index.ts
@@ -89,7 +89,7 @@ export class ReplHistoryItem extends Model({
}
@modelAction
- setResult(status: string, hasResult: boolean, implicitLimit: number) {
+ setResult(status: string, hasResult: boolean, implicitLimit: number | null) {
this.hasResult = hasResult;
this.status = status;
this.implicitLimit = implicitLimit;
@@ -202,28 +202,52 @@ export class Repl extends Model({
initialScrollPos = 0;
+ // -1 is draft, then counts how many history items back from most recent
historyCursor = -1;
draftQuery = Text.empty;
+ dedupedQueryHistory: ReplHistoryItem[] = [];
+
+ _addDedupedHistoryQueries(history: ReplHistoryItem[]) {
+ let lastQuery =
+ this.dedupedQueryHistory[this.dedupedQueryHistory.length - 1]?.query;
+ for (let i = history.length - 1; i >= 0; i--) {
+ const item = history[i];
+ if (item.query !== lastQuery) {
+ this.dedupedQueryHistory.push(item);
+ lastQuery = item.query;
+ }
+ }
+ }
+
@action
navigateHistory(direction: 1 | -1) {
- let cursor =
- (this.historyCursor === -1
- ? this.queryHistory.length
- : this.historyCursor) + direction;
+ // 1 => backwards, -1 => forwards
+ if (this._fetchingHistory) {
+ return;
+ }
+ let cursor = this.historyCursor + direction;
- if (cursor === this.queryHistory.length) {
+ if (
+ cursor < -1 ||
+ (cursor >= this.dedupedQueryHistory.length && !this._hasUnfetchedHistory)
+ ) {
+ return;
+ }
+ if (cursor === -1) {
this.currentQuery = this.draftQuery;
this.historyCursor = -1;
} else {
- const historyItem = this.queryHistory[cursor];
- if (historyItem) {
- if (this.historyCursor === -1) {
- this.draftQuery = this.currentQuery;
- }
- this.currentQuery = Text.of(historyItem.query.split("\n"));
- this.historyCursor = cursor;
+ if (cursor >= this.dedupedQueryHistory.length) {
+ this.fetchReplHistory().then(() => this.navigateHistory(1));
+ return;
+ }
+ const historyItem = this.dedupedQueryHistory[cursor];
+ if (this.historyCursor === -1) {
+ this.draftQuery = this.currentQuery;
}
+ this.currentQuery = Text.of(historyItem.query.split("\n"));
+ this.historyCursor = cursor;
}
}
@@ -293,6 +317,7 @@ export class Repl extends Model({
this.itemHeights.addHistoryItems(
Array(history.length).fill(defaultItemHeight)
);
+ this._addDedupedHistoryQueries(historyItems);
this._fetchingHistory = false;
});
@@ -363,7 +388,7 @@ export class Repl extends Model({
if (isCommandQuery) {
yield* _await(handleSlashCommand(query, this, historyItem));
} else {
- const implicitLimit = sessionStateCtx
+ const implicitLimitConfig = sessionStateCtx
.get(this)!
.activeState.options.find(
(opt) => opt.name === "Implicit Limit"
@@ -376,8 +401,8 @@ export class Repl extends Model({
undefined,
{
implicitLimit:
- implicitLimit != null
- ? implicitLimit + BigInt(1)
+ implicitLimitConfig != null
+ ? implicitLimitConfig + BigInt(1)
: undefined,
},
(this._runningQuery as AbortController).signal
@@ -386,7 +411,9 @@ export class Repl extends Model({
dbState.refreshCaches(capabilities, status ? [status] : []);
- historyItem.setResult(status, !!result, Number(implicitLimit));
+ const implicitLimit =
+ implicitLimitConfig != null ? Number(implicitLimitConfig) : null;
+ historyItem.setResult(status, !!result, implicitLimit);
if (result) {
if (
status === ExplainStateType.explain ||
@@ -399,7 +426,7 @@ export class Repl extends Model({
} else {
this.resultInspectorCache.set(
historyItem.$modelId,
- createInspector(result, Number(implicitLimit), (item) =>
+ createInspector(result, implicitLimit, (item) =>
this.setExtendedViewerItem(item)
)
);
diff --git a/web/tests/_globalSetup.ts b/web/tests/_globalSetup.ts
index 5630c722..fb5057a6 100644
--- a/web/tests/_globalSetup.ts
+++ b/web/tests/_globalSetup.ts
@@ -34,15 +34,23 @@ function checkEdgeDBServerAlive() {
});
}
-async function waitUntilAlive(check: () => Promise, event: Event) {
+async function waitUntilAlive(
+ check: () => Promise,
+ errMessage: string,
+ event?: Event
+) {
for (let i = 0; i < STARTUP_TIMEOUT / 1000; i++) {
if (await check()) {
- event.set();
+ event?.set();
return;
}
await sleep(1000);
}
- event.setError("EdgeDB server startup timed out");
+ if (event) {
+ event.setError(errMessage);
+ } else {
+ throw new Error(errMessage);
+ }
}
async function checkUIServerAlive() {
@@ -61,14 +69,31 @@ async function checkUIServerAlive() {
});
}
+async function checkConfigApplied() {
+ try {
+ const res = await fetch("http://localhost:5656/server-info");
+ if (res.ok) {
+ const info = await res.json();
+ if (info.instance_config.cors_allow_origins.length > 0) {
+ return true;
+ }
+ }
+ return false;
+ } catch {
+ return false;
+ }
+}
+
export default async function globalSetup() {
console.log("\n");
let edbServerProc: ChildProcess | null = null;
const edbServerAlive = new Event();
+ let usingExistingDevServer = false;
if (await checkEdgeDBServerAlive()) {
console.log("Re-using EdgeDB server already running on 5656");
+ usingExistingDevServer = true;
edbServerAlive.set();
} else {
console.log("Starting EdgeDB server...");
@@ -84,9 +109,7 @@ export default async function globalSetup() {
]
: ["--devmode"];
- edbServerProc = spawn(srvcmd, args, {
- env: {...process.env, EDGEDB_DEBUG_HTTP_INJECT_CORS: "1"},
- }) as ChildProcess;
+ edbServerProc = spawn(srvcmd, args) as ChildProcess;
edbServerProc.once("close", (code) => {
if (!edbServerAlive.done) {
edbServerAlive.setError(
@@ -94,7 +117,11 @@ export default async function globalSetup() {
);
}
});
- waitUntilAlive(checkEdgeDBServerAlive, edbServerAlive);
+ waitUntilAlive(
+ checkEdgeDBServerAlive,
+ "EdgeDB server startup timed out",
+ edbServerAlive
+ );
}
let uiServerProc: ChildProcess | null = null;
@@ -116,7 +143,11 @@ export default async function globalSetup() {
);
}
});
- waitUntilAlive(checkUIServerAlive, uiServerAlive);
+ waitUntilAlive(
+ checkUIServerAlive,
+ "UI server startup timed out",
+ uiServerAlive
+ );
}
await Promise.all([
@@ -146,6 +177,12 @@ export default async function globalSetup() {
try {
await testClient.execute(schemaScript);
+ if (!usingExistingDevServer) {
+ await testClient.execute(
+ `configure instance set cors_allow_origins := {'*'}`
+ );
+ await waitUntilAlive(checkConfigApplied, "Config apply timed out");
+ }
break;
} catch (err) {
if (!(err instanceof AccessError)) {