Add Rails backend: accounts via magic link, server documents, sharing
The app becomes a Rails 8 application; the Vue frontend moves to app/frontend and is served through vite_rails. - Without an account documents stay in the browser (IndexedDB). Sign-in is by magic link or the 6-digit code from the same mail; afterwards local documents can be uploaded into the account. - Server documents (/d/<id>) with versioned saves and a single-editor lock per browser tab; others see them read-only and follow by polling. - Sharing via link (works without an account) and via e-mail invitation, roles viewer/editor; the owner manages links and people. - Store gains a read-only mode that blocks all content changes. - Solid Queue, Cache and Cable share the one PostgreSQL database. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseFormula, parseFunctionBlock, parseMacroBlock } from '../../engine';
|
||||
import { CODE_REFERENCE, matchesQuery, referenceFor } from '../codeReference';
|
||||
|
||||
describe('CODE_REFERENCE', () => {
|
||||
it('documents every macro write action', () => {
|
||||
const names = new Set(referenceFor('macro').map((e) => e.name));
|
||||
for (const a of [
|
||||
'SETVALUE', 'INSERTROW', 'DELETEROW', 'INSERTCOL', 'DELETECOL', 'ADDTABLE',
|
||||
'RENAMETABLE', 'DELETETABLE', 'ADDSHEET', 'RENAMESHEET', 'DELETESHEET',
|
||||
'GET', 'POST', 'JSONGET',
|
||||
]) {
|
||||
expect(names.has(a)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('every example (except "…" fragments) parses in each context it is listed for', () => {
|
||||
// the parameter names the function examples use; in a function body only
|
||||
// control-flow examples are whole statement sequences, the rest are
|
||||
// expressions (usable anywhere inside one)
|
||||
const params = ['bereich', 'umsatz', 'kosten'];
|
||||
for (const e of CODE_REFERENCE) {
|
||||
if (!e.example || e.example.includes('…')) continue;
|
||||
const ex = e.example;
|
||||
for (const ctx of e.contexts) {
|
||||
const parse =
|
||||
ctx === 'macro'
|
||||
? () => parseMacroBlock(ex)
|
||||
: ctx === 'block' && e.category === 'Ablauf'
|
||||
? () => parseFunctionBlock(ex, params)
|
||||
: () => parseFormula(ex, params);
|
||||
expect(parse, `${ctx} · ${e.name}: ${ex}`).not.toThrow();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('function editors never offer write actions or network calls', () => {
|
||||
for (const ctx of ['block', 'expression'] as const) {
|
||||
const names = referenceFor(ctx).map((e) => e.name);
|
||||
for (const a of ['SETVALUE', 'INSERTROW', 'ADDSHEET', 'GET', 'POST', 'JSONGET']) {
|
||||
expect(names).not.toContain(a);
|
||||
}
|
||||
}
|
||||
// single formulas have no statements at all
|
||||
expect(referenceFor('expression').some((e) => e.category === 'Ablauf')).toBe(false);
|
||||
});
|
||||
|
||||
it('search matches synonyms and requires every word', () => {
|
||||
const hits = (q: string) => referenceFor('macro').filter((e) => matchesQuery(e, q)).map((e) => e.name);
|
||||
expect(hits('zeile einfügen')).toContain('INSERTROW');
|
||||
expect(hits('zeile einfügen')).not.toContain('DELETEROW');
|
||||
expect(hits('schleife')).toContain('FOR … IN … DO … END');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user