@elytracms/studio 0.0.47 → 0.0.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/convex/auth.ts +61 -12
- package/convex/authority.ts +40 -2
- package/convex/functions.ts +9 -0
- package/convex/invites.ts +75 -0
- package/convex/members.ts +44 -18
- package/convex/schema.ts +37 -0
- package/convex/users.ts +89 -2
- package/dist/components/account/account-settings.d.ts +13 -0
- package/dist/components/account/account-settings.js +54 -0
- package/dist/components/account/account-settings.js.map +1 -0
- package/dist/components/account/credential-panel.d.ts +14 -0
- package/dist/components/account/credential-panel.js +52 -0
- package/dist/components/account/credential-panel.js.map +1 -0
- package/dist/components/account/self-account-schema.d.ts +19 -0
- package/dist/components/account/self-account-schema.js +52 -0
- package/dist/components/account/self-account-schema.js.map +1 -0
- package/dist/components/account/self-account-view-source.d.ts +35 -0
- package/dist/components/account/self-account-view-source.js +110 -0
- package/dist/components/account/self-account-view-source.js.map +1 -0
- package/dist/components/auth-context.d.ts +5 -1
- package/dist/components/auth-context.js +9 -0
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +7 -2
- package/dist/components/cms/cms-store.js +35 -4
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/view-bar.js +4 -2
- package/dist/components/cms/view-bar.js.map +1 -1
- package/dist/components/current-user-menu.js +11 -2
- package/dist/components/current-user-menu.js.map +1 -1
- package/dist/components/deployment-setup-gate.d.ts +31 -0
- package/dist/components/deployment-setup-gate.js +92 -0
- package/dist/components/deployment-setup-gate.js.map +1 -0
- package/dist/components/project-session.d.ts +1 -1
- package/dist/components/project-session.js.map +1 -1
- package/dist/components/use-hydration-phase.js +1 -1
- package/dist/components/use-hydration-phase.js.map +1 -1
- package/dist/components/users/invite-user-bar.d.ts +3 -0
- package/dist/components/users/invite-user-bar.js +91 -0
- package/dist/components/users/invite-user-bar.js.map +1 -0
- package/dist/components/users/users-module.js +13 -2
- package/dist/components/users/users-module.js.map +1 -1
- package/dist/lib/auth/auth-adapter.d.ts +50 -2
- package/dist/lib/auth/auth-adapter.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +28 -1
- package/dist/lib/auth/convex-auth-adapter.js +113 -1
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +3 -1
- package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
- package/dist/lib/auth/index.d.ts +2 -2
- package/dist/lib/auth/index.js +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/auth/local-auth-adapter.d.ts +3 -1
- package/dist/lib/auth/local-auth-adapter.js +16 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -1
- package/dist/lib/cms-views/view-model.d.ts +33 -4
- package/dist/lib/cms-views/view-model.js +66 -10
- package/dist/lib/cms-views/view-model.js.map +1 -1
- package/dist/lib/collection-view/collection-view-source.d.ts +3 -2
- package/dist/lib/collection-view/collection-view-source.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +45 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/operations/studio-operations.js +1 -1
- package/dist/lib/operations/studio-operations.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +1 -1
- package/dist/lib/persistence/backend-mode.js +1 -1
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.d.ts +2 -1
- package/dist/lib/persistence/convex-adapter.js +69 -1
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/deployment-readiness.d.ts +36 -0
- package/dist/lib/persistence/deployment-readiness.js +26 -0
- package/dist/lib/persistence/deployment-readiness.js.map +1 -0
- package/dist/lib/persistence/index.d.ts +3 -1
- package/dist/lib/persistence/index.js +4 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/workspace/switcher-source.js +1 -1
- package/dist/lib/workspace/switcher-source.js.map +1 -1
- package/dist/lib/workspace/workspace-source.d.ts +1 -1
- package/dist/lib/workspace/workspace-source.js +1 -1
- package/dist/lib/workspace/workspace-source.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-panel.js","sourceRoot":"","sources":["../../../src/components/account/credential-panel.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useState } from 'react'\nimport type { FormEvent } from 'react'\nimport { KeyRound, ShieldCheck } from 'lucide-react'\nimport {\n Button,\n Card,\n CardContent,\n Field,\n Heading,\n Input,\n Row,\n Stack,\n Text,\n toast,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useAuth } from '../auth-context'\n\n/**\n * The credential seam (EC-240 H-tail item 3) — part of the account **property\n * surface**, never a modal. Two blocks:\n *\n * - **Sign-in method** (read-only): today every studio account is email + password.\n * OAuth \"connected accounts\" is a deferred extension.\n * - **Change password**: current + new + confirm, handed straight to\n * `AuthAdapter.changePassword` (provider-delegated). SEAM ONLY — the raw values\n * are never logged, stored, or inspected here; they leave in the adapter call.\n *\n * The current-password field is the gate around this sensitive action (no separate\n * confirm dialog needed).\n */\nexport function CredentialPanel() {\n const auth = useAuth()\n const [current, setCurrent] = useState('')\n const [next, setNext] = useState('')\n const [confirm, setConfirm] = useState('')\n const [phase, setPhase] = useState<'idle' | 'submitting'>('idle')\n const [error, setError] = useState<string | null>(null)\n\n async function handleSubmit(event: FormEvent<HTMLFormElement>) {\n event.preventDefault()\n setError(null)\n if (next !== confirm) {\n setError(translate('account.password.mismatch'))\n return\n }\n setPhase('submitting')\n // The raw values leave here ONLY into the provider adapter — never logged.\n const result = await auth.changePassword({ currentPassword: current, newPassword: next })\n setPhase('idle')\n if (result.ok) {\n setCurrent('')\n setNext('')\n setConfirm('')\n toast.success(translate('account.password.changed'))\n return\n }\n setError(result.message)\n }\n\n const submitting = phase === 'submitting'\n\n return (\n <Card data-testid=\"credential-panel\">\n <CardContent className=\"pt-5\">\n <Stack gap=\"4\">\n <Stack gap=\"1\">\n <Row gap=\"2\" align=\"center\">\n <ShieldCheck className=\"size-4 text-muted-foreground\" />\n <Heading level={3} size=\"h4\">\n {translate('account.security.title')}\n </Heading>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {translate('account.security.method')}\n </Text>\n </Stack>\n\n <form onSubmit={handleSubmit} noValidate>\n <Stack gap=\"3\">\n <Field\n label={translate('account.password.current')}\n htmlFor=\"account-current-password\"\n required\n >\n <Input\n id=\"account-current-password\"\n type=\"password\"\n autoComplete=\"current-password\"\n value={current}\n onChange={(e) => setCurrent(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Field\n label={translate('account.password.new')}\n htmlFor=\"account-new-password\"\n required\n >\n <Input\n id=\"account-new-password\"\n type=\"password\"\n autoComplete=\"new-password\"\n value={next}\n onChange={(e) => setNext(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Field\n label={translate('account.password.confirm')}\n htmlFor=\"account-confirm-password\"\n required\n error={error}\n >\n <Input\n id=\"account-confirm-password\"\n type=\"password\"\n autoComplete=\"new-password\"\n value={confirm}\n onChange={(e) => setConfirm(e.target.value)}\n disabled={submitting}\n />\n </Field>\n <Row>\n <Button\n type=\"submit\"\n variant=\"outline\"\n disabled={submitting || !current || !next || !confirm}\n >\n <KeyRound />\n {submitting\n ? translate('account.password.changing')\n : translate('account.password.change')}\n </Button>\n </Row>\n </Stack>\n </form>\n </Stack>\n </CardContent>\n </Card>\n )\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { CollectionDef } from '@elytracms/core/cms-core';
|
|
2
|
+
/**
|
|
3
|
+
* The **account-settings** field lens (EC-240 H-tail) — the schema the portable
|
|
4
|
+
* edit shell renders when a member opens their OWN record (via the user menu). It
|
|
5
|
+
* diverges from the admin `usersCollectionDef` in exactly the way the ratified
|
|
6
|
+
* scope dictates:
|
|
7
|
+
*
|
|
8
|
+
* - **name / avatar are EDITABLE** (not `form.readOnly`) — the person edits their
|
|
9
|
+
* own identity; the source's `save` routes these to `AuthAdapter.updateProfile`
|
|
10
|
+
* (auth-owned), never the members rail.
|
|
11
|
+
* - **email is read-only** (email change is a separate, deferred seam).
|
|
12
|
+
* - **role is read-only** — you never change your own role (the server integrity
|
|
13
|
+
* floor also guards it).
|
|
14
|
+
*
|
|
15
|
+
* Avatar is a plain URL text field for now; the media-picker convention is a
|
|
16
|
+
* deferred polish item — this keeps the mechanism (name + avatar → updateProfile)
|
|
17
|
+
* honest and complete without a half-built picker.
|
|
18
|
+
*/
|
|
19
|
+
export declare const selfAccountCollectionDef: CollectionDef;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { collectionDefSchema } from '@elytracms/core/cms-core';
|
|
2
|
+
import { roleOptions } from '@elytracms/core/persistence';
|
|
3
|
+
import { msgLabel } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
/**
|
|
5
|
+
* The **account-settings** field lens (EC-240 H-tail) — the schema the portable
|
|
6
|
+
* edit shell renders when a member opens their OWN record (via the user menu). It
|
|
7
|
+
* diverges from the admin `usersCollectionDef` in exactly the way the ratified
|
|
8
|
+
* scope dictates:
|
|
9
|
+
*
|
|
10
|
+
* - **name / avatar are EDITABLE** (not `form.readOnly`) — the person edits their
|
|
11
|
+
* own identity; the source's `save` routes these to `AuthAdapter.updateProfile`
|
|
12
|
+
* (auth-owned), never the members rail.
|
|
13
|
+
* - **email is read-only** (email change is a separate, deferred seam).
|
|
14
|
+
* - **role is read-only** — you never change your own role (the server integrity
|
|
15
|
+
* floor also guards it).
|
|
16
|
+
*
|
|
17
|
+
* Avatar is a plain URL text field for now; the media-picker convention is a
|
|
18
|
+
* deferred polish item — this keeps the mechanism (name + avatar → updateProfile)
|
|
19
|
+
* honest and complete without a half-built picker.
|
|
20
|
+
*/
|
|
21
|
+
export const selfAccountCollectionDef = collectionDefSchema.parse({
|
|
22
|
+
id: 'account',
|
|
23
|
+
kind: 'document',
|
|
24
|
+
titleField: 'name',
|
|
25
|
+
form: {
|
|
26
|
+
label: msgLabel('account.rail.label'),
|
|
27
|
+
labelSingular: msgLabel('account.rail.labelSingular'),
|
|
28
|
+
},
|
|
29
|
+
fields: [
|
|
30
|
+
{ name: 'name', type: 'text', form: { label: msgLabel('account.field.name.label') } },
|
|
31
|
+
{
|
|
32
|
+
name: 'avatar',
|
|
33
|
+
type: 'text',
|
|
34
|
+
form: {
|
|
35
|
+
label: msgLabel('account.field.avatar.label'),
|
|
36
|
+
description: msgLabel('account.field.avatar.hint'),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'email',
|
|
41
|
+
type: 'text',
|
|
42
|
+
form: { label: msgLabel('account.field.email.label'), readOnly: true },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'role',
|
|
46
|
+
type: 'select',
|
|
47
|
+
options: [...roleOptions()],
|
|
48
|
+
form: { label: msgLabel('account.field.role.label'), readOnly: true },
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
//# sourceMappingURL=self-account-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-account-schema.js","sourceRoot":"","sources":["../../../src/components/account/self-account-schema.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAA;AAE9D,OAAO,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAA;AACzD,OAAO,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAA;AAErD;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAkB,mBAAmB,CAAC,KAAK,CAAC;IAC/E,EAAE,EAAE,SAAS;IACb,IAAI,EAAE,UAAU;IAChB,UAAU,EAAE,MAAM;IAClB,IAAI,EAAE;QACJ,KAAK,EAAE,QAAQ,CAAC,oBAAoB,CAAC;QACrC,aAAa,EAAE,QAAQ,CAAC,4BAA4B,CAAC;KACtD;IACD,MAAM,EAAE;QACN,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,0BAA0B,CAAC,EAAE,EAAE;QACrF;YACE,IAAI,EAAE,QAAQ;YACd,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE;gBACJ,KAAK,EAAE,QAAQ,CAAC,4BAA4B,CAAC;gBAC7C,WAAW,EAAE,QAAQ,CAAC,2BAA2B,CAAC;aACnD;SACF;QACD;YACE,IAAI,EAAE,OAAO;YACb,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,2BAA2B,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;SACvE;QACD;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,QAAQ;YACd,OAAO,EAAE,CAAC,GAAG,WAAW,EAAE,CAAC;YAC3B,IAAI,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,0BAA0B,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE;SACtE;KACF;CACF,CAAC,CAAA","sourcesContent":["import { collectionDefSchema } from '@elytracms/core/cms-core'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { roleOptions } from '@elytracms/core/persistence'\nimport { msgLabel } from '@elytracms/studio/lib/i18n'\n\n/**\n * The **account-settings** field lens (EC-240 H-tail) — the schema the portable\n * edit shell renders when a member opens their OWN record (via the user menu). It\n * diverges from the admin `usersCollectionDef` in exactly the way the ratified\n * scope dictates:\n *\n * - **name / avatar are EDITABLE** (not `form.readOnly`) — the person edits their\n * own identity; the source's `save` routes these to `AuthAdapter.updateProfile`\n * (auth-owned), never the members rail.\n * - **email is read-only** (email change is a separate, deferred seam).\n * - **role is read-only** — you never change your own role (the server integrity\n * floor also guards it).\n *\n * Avatar is a plain URL text field for now; the media-picker convention is a\n * deferred polish item — this keeps the mechanism (name + avatar → updateProfile)\n * honest and complete without a half-built picker.\n */\nexport const selfAccountCollectionDef: CollectionDef = collectionDefSchema.parse({\n id: 'account',\n kind: 'document',\n titleField: 'name',\n form: {\n label: msgLabel('account.rail.label'),\n labelSingular: msgLabel('account.rail.labelSingular'),\n },\n fields: [\n { name: 'name', type: 'text', form: { label: msgLabel('account.field.name.label') } },\n {\n name: 'avatar',\n type: 'text',\n form: {\n label: msgLabel('account.field.avatar.label'),\n description: msgLabel('account.field.avatar.hint'),\n },\n },\n {\n name: 'email',\n type: 'text',\n form: { label: msgLabel('account.field.email.label'), readOnly: true },\n },\n {\n name: 'role',\n type: 'select',\n options: [...roleOptions()],\n form: { label: msgLabel('account.field.role.label'), readOnly: true },\n },\n ],\n})\n"]}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CmsDocument } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { UserRecord } from '@elytracms/core/persistence';
|
|
3
|
+
import type { CollectionViewCapabilities, CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
4
|
+
/**
|
|
5
|
+
* The capability profile of the account surface (EC-240 H-tail): a single self
|
|
6
|
+
* record edited in place — no publish/versions/create/remove/import. The record
|
|
7
|
+
* is the caller's own; there is nothing to delete or list.
|
|
8
|
+
*/
|
|
9
|
+
export declare const ACCOUNT_CAPABILITIES: CollectionViewCapabilities;
|
|
10
|
+
/** Project the self record into the account lens's `values`. */
|
|
11
|
+
export declare function selfToDocument(record: UserRecord): CmsDocument;
|
|
12
|
+
/**
|
|
13
|
+
* Compute the identity diff (name / avatar) an edited account document carries —
|
|
14
|
+
* the exact seam handed to `AuthAdapter.updateProfile`. Pure so the split-write is
|
|
15
|
+
* unit-testable: only the fields the person changed are sent (an empty diff is a
|
|
16
|
+
* no-op the caller can short-circuit). Never carries `email`/`role` — those are
|
|
17
|
+
* read-only on this surface and are not auth-adapter-writable here.
|
|
18
|
+
*/
|
|
19
|
+
export declare function accountIdentityDiff(doc: CmsDocument, previous: UserRecord): {
|
|
20
|
+
name?: string;
|
|
21
|
+
avatar?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Assemble a {@link CollectionViewSource} for the **account-settings** surface
|
|
25
|
+
* (EC-240 H-tail). Reuses the portable edit shell over the caller's OWN record —
|
|
26
|
+
* name/avatar editable, email/role read-only (per the schema). The `save` routes
|
|
27
|
+
* the identity diff to `AuthAdapter.updateProfile` (auth-owned) — NOT the
|
|
28
|
+
* members-rail `users.save` — then refreshes the loaded record. `EDITABLE_IDENTITY`
|
|
29
|
+
* marks which fields the save may write; everything else is display-only.
|
|
30
|
+
*/
|
|
31
|
+
export declare function useSelfAccountViewSource(input: {
|
|
32
|
+
projectId: string;
|
|
33
|
+
self: UserRecord;
|
|
34
|
+
refresh: () => Promise<void>;
|
|
35
|
+
}): CollectionViewSource;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { studioPath } from '@elytracms/studio/router';
|
|
3
|
+
import { useAuth } from '../auth-context';
|
|
4
|
+
import { selfAccountCollectionDef } from './self-account-schema';
|
|
5
|
+
import { normalizeOkResult, useRailViewSource } from '../rail-view-source';
|
|
6
|
+
/** The account surface is not localized — one implicit locale keeps the lens honest. */
|
|
7
|
+
const ACCOUNT_LOCALE = 'en';
|
|
8
|
+
const ACCOUNT_LOCALE_CONFIG = { default: ACCOUNT_LOCALE, locales: [ACCOUNT_LOCALE] };
|
|
9
|
+
/**
|
|
10
|
+
* The capability profile of the account surface (EC-240 H-tail): a single self
|
|
11
|
+
* record edited in place — no publish/versions/create/remove/import. The record
|
|
12
|
+
* is the caller's own; there is nothing to delete or list.
|
|
13
|
+
*/
|
|
14
|
+
export const ACCOUNT_CAPABILITIES = {
|
|
15
|
+
publish: false,
|
|
16
|
+
versions: false,
|
|
17
|
+
locales: false,
|
|
18
|
+
inlineEdit: false,
|
|
19
|
+
create: false,
|
|
20
|
+
remove: false,
|
|
21
|
+
bulk: false,
|
|
22
|
+
liveUrl: false,
|
|
23
|
+
hierarchy: false,
|
|
24
|
+
importExport: false,
|
|
25
|
+
};
|
|
26
|
+
/** Everything in `values` that is NOT an editable identity field is display-only. */
|
|
27
|
+
const EDITABLE_IDENTITY = new Set(['name', 'avatar']);
|
|
28
|
+
/** Project the self record into the account lens's `values`. */
|
|
29
|
+
export function selfToDocument(record) {
|
|
30
|
+
return {
|
|
31
|
+
collection: selfAccountCollectionDef.id,
|
|
32
|
+
id: record.id,
|
|
33
|
+
values: {
|
|
34
|
+
name: record.name ?? '',
|
|
35
|
+
avatar: record.avatar ?? '',
|
|
36
|
+
email: record.email,
|
|
37
|
+
role: record.roles[0] ?? 'viewer',
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compute the identity diff (name / avatar) an edited account document carries —
|
|
43
|
+
* the exact seam handed to `AuthAdapter.updateProfile`. Pure so the split-write is
|
|
44
|
+
* unit-testable: only the fields the person changed are sent (an empty diff is a
|
|
45
|
+
* no-op the caller can short-circuit). Never carries `email`/`role` — those are
|
|
46
|
+
* read-only on this surface and are not auth-adapter-writable here.
|
|
47
|
+
*/
|
|
48
|
+
export function accountIdentityDiff(doc, previous) {
|
|
49
|
+
const values = doc.values ?? {};
|
|
50
|
+
const diff = {};
|
|
51
|
+
const nextName = typeof values.name === 'string' ? values.name : '';
|
|
52
|
+
const nextAvatar = typeof values.avatar === 'string' ? values.avatar : '';
|
|
53
|
+
if (nextName !== (previous.name ?? ''))
|
|
54
|
+
diff.name = nextName;
|
|
55
|
+
if (nextAvatar !== (previous.avatar ?? ''))
|
|
56
|
+
diff.avatar = nextAvatar;
|
|
57
|
+
return diff;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Assemble a {@link CollectionViewSource} for the **account-settings** surface
|
|
61
|
+
* (EC-240 H-tail). Reuses the portable edit shell over the caller's OWN record —
|
|
62
|
+
* name/avatar editable, email/role read-only (per the schema). The `save` routes
|
|
63
|
+
* the identity diff to `AuthAdapter.updateProfile` (auth-owned) — NOT the
|
|
64
|
+
* members-rail `users.save` — then refreshes the loaded record. `EDITABLE_IDENTITY`
|
|
65
|
+
* marks which fields the save may write; everything else is display-only.
|
|
66
|
+
*/
|
|
67
|
+
export function useSelfAccountViewSource(input) {
|
|
68
|
+
const { projectId, self, refresh } = input;
|
|
69
|
+
const auth = useAuth();
|
|
70
|
+
const documents = useCallback(() => [selfToDocument(self)], [self]);
|
|
71
|
+
const get = useCallback((ref) => ref.id === self.id ? selfToDocument(self) : undefined, [self]);
|
|
72
|
+
const titleOf = useCallback(() => self.name || self.email || self.id, [self]);
|
|
73
|
+
const save = useCallback(async (doc) => {
|
|
74
|
+
const diff = accountIdentityDiff(doc, self);
|
|
75
|
+
// No identity change (e.g. a read-only-only edit) → nothing to write.
|
|
76
|
+
if (diff.name === undefined && diff.avatar === undefined)
|
|
77
|
+
return { ok: true };
|
|
78
|
+
const result = await auth.updateProfile(diff);
|
|
79
|
+
if (!result.ok)
|
|
80
|
+
return { ok: false, message: result.message };
|
|
81
|
+
// Reflect the new identity in the loaded record (session already refreshed).
|
|
82
|
+
await refresh();
|
|
83
|
+
return { ok: true };
|
|
84
|
+
}, [auth, self, refresh]);
|
|
85
|
+
const noRemove = useCallback(async () => ({ ok: true }), []);
|
|
86
|
+
const noValidation = useCallback(() => [], []);
|
|
87
|
+
const createUnsupported = useCallback(() => {
|
|
88
|
+
throw new Error('the account surface has no create');
|
|
89
|
+
}, []);
|
|
90
|
+
return useRailViewSource({
|
|
91
|
+
projectId,
|
|
92
|
+
schema: selfAccountCollectionDef,
|
|
93
|
+
capabilities: ACCOUNT_CAPABILITIES,
|
|
94
|
+
localeConfig: ACCOUNT_LOCALE_CONFIG,
|
|
95
|
+
documents,
|
|
96
|
+
get,
|
|
97
|
+
titleOf,
|
|
98
|
+
// Editable-identity fields drive writability; the rest are read-only per schema.
|
|
99
|
+
isFieldReadOnly: (field) => !EDITABLE_IDENTITY.has(field.name) || (field.form?.readOnly ?? false),
|
|
100
|
+
detailPath: (id) => studioPath.user(id),
|
|
101
|
+
listPath: studioPath.users(),
|
|
102
|
+
validate: noValidation,
|
|
103
|
+
create: createUnsupported,
|
|
104
|
+
save,
|
|
105
|
+
remove: noRemove,
|
|
106
|
+
normalizeSaveResult: normalizeOkResult,
|
|
107
|
+
normalizeRemoveResult: () => null,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=self-account-view-source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"self-account-view-source.js","sourceRoot":"","sources":["../../../src/components/account/self-account-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback } from 'react'\nimport type { CmsDocument, CmsValidationIssue, DocumentRef, Locale, LocaleConfig } from '@elytracms/core/cms-core'\nimport type { UserRecord } from '@elytracms/core/persistence'\nimport { studioPath } from '@elytracms/studio/router'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { useAuth } from '../auth-context'\nimport { selfAccountCollectionDef } from './self-account-schema'\nimport { normalizeOkResult, useRailViewSource } from '../rail-view-source'\n\n/** The account surface is not localized — one implicit locale keeps the lens honest. */\nconst ACCOUNT_LOCALE: Locale = 'en'\nconst ACCOUNT_LOCALE_CONFIG: LocaleConfig = { default: ACCOUNT_LOCALE, locales: [ACCOUNT_LOCALE] }\n\n/**\n * The capability profile of the account surface (EC-240 H-tail): a single self\n * record edited in place — no publish/versions/create/remove/import. The record\n * is the caller's own; there is nothing to delete or list.\n */\nexport const ACCOUNT_CAPABILITIES: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: false,\n remove: false,\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n}\n\n/** Everything in `values` that is NOT an editable identity field is display-only. */\nconst EDITABLE_IDENTITY = new Set(['name', 'avatar'])\n\n/** Project the self record into the account lens's `values`. */\nexport function selfToDocument(record: UserRecord): CmsDocument {\n return {\n collection: selfAccountCollectionDef.id,\n id: record.id,\n values: {\n name: record.name ?? '',\n avatar: record.avatar ?? '',\n email: record.email,\n role: record.roles[0] ?? 'viewer',\n },\n }\n}\n\n/**\n * Compute the identity diff (name / avatar) an edited account document carries —\n * the exact seam handed to `AuthAdapter.updateProfile`. Pure so the split-write is\n * unit-testable: only the fields the person changed are sent (an empty diff is a\n * no-op the caller can short-circuit). Never carries `email`/`role` — those are\n * read-only on this surface and are not auth-adapter-writable here.\n */\nexport function accountIdentityDiff(\n doc: CmsDocument,\n previous: UserRecord,\n): { name?: string; avatar?: string } {\n const values = doc.values ?? {}\n const diff: { name?: string; avatar?: string } = {}\n const nextName = typeof values.name === 'string' ? values.name : ''\n const nextAvatar = typeof values.avatar === 'string' ? values.avatar : ''\n if (nextName !== (previous.name ?? '')) diff.name = nextName\n if (nextAvatar !== (previous.avatar ?? '')) diff.avatar = nextAvatar\n return diff\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **account-settings** surface\n * (EC-240 H-tail). Reuses the portable edit shell over the caller's OWN record —\n * name/avatar editable, email/role read-only (per the schema). The `save` routes\n * the identity diff to `AuthAdapter.updateProfile` (auth-owned) — NOT the\n * members-rail `users.save` — then refreshes the loaded record. `EDITABLE_IDENTITY`\n * marks which fields the save may write; everything else is display-only.\n */\nexport function useSelfAccountViewSource(input: {\n projectId: string\n self: UserRecord\n refresh: () => Promise<void>\n}): CollectionViewSource {\n const { projectId, self, refresh } = input\n const auth = useAuth()\n\n const documents = useCallback(() => [selfToDocument(self)], [self])\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined =>\n ref.id === self.id ? selfToDocument(self) : undefined,\n [self],\n )\n const titleOf = useCallback(() => self.name || self.email || self.id, [self])\n\n const save = useCallback(\n async (doc: CmsDocument): Promise<{ ok: true } | { ok: false; message: string }> => {\n const diff = accountIdentityDiff(doc, self)\n // No identity change (e.g. a read-only-only edit) → nothing to write.\n if (diff.name === undefined && diff.avatar === undefined) return { ok: true }\n const result = await auth.updateProfile(diff)\n if (!result.ok) return { ok: false, message: result.message }\n // Reflect the new identity in the loaded record (session already refreshed).\n await refresh()\n return { ok: true }\n },\n [auth, self, refresh],\n )\n\n const noRemove = useCallback(\n async (): Promise<{ ok: true }> => ({ ok: true }),\n [],\n )\n const noValidation = useCallback((): CmsValidationIssue[] => [], [])\n const createUnsupported = useCallback((): DocumentRef => {\n throw new Error('the account surface has no create')\n }, [])\n\n return useRailViewSource({\n projectId,\n schema: selfAccountCollectionDef,\n capabilities: ACCOUNT_CAPABILITIES,\n localeConfig: ACCOUNT_LOCALE_CONFIG,\n documents,\n get,\n titleOf,\n // Editable-identity fields drive writability; the rest are read-only per schema.\n isFieldReadOnly: (field) => !EDITABLE_IDENTITY.has(field.name) || (field.form?.readOnly ?? false),\n detailPath: (id: string) => studioPath.user(id),\n listPath: studioPath.users(),\n validate: noValidation,\n create: createUnsupported,\n save,\n remove: noRemove,\n normalizeSaveResult: normalizeOkResult,\n normalizeRemoveResult: (): OperationError | null => null,\n })\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
2
|
import type { ProjectRole } from '@elytracms/core/persistence';
|
|
3
|
-
import type { AuthAdapter, AuthSession, AuthStatus, SignInResult } from '@elytracms/studio/lib/auth';
|
|
3
|
+
import type { AuthAdapter, AuthSession, AuthStatus, ChangePasswordInput, ChangePasswordResult, SignInResult, UpdateProfileInput, UpdateProfileResult } from '@elytracms/studio/lib/auth';
|
|
4
4
|
/**
|
|
5
5
|
* Session store (EC-150). Owns the one `AuthAdapter` the app talks to, keeps
|
|
6
6
|
* the session in React state, and binds/unbinds the operations-layer principal
|
|
@@ -15,6 +15,10 @@ export interface AuthStore {
|
|
|
15
15
|
/** Register the first/new user on the active backend, then sign in (EC-179). */
|
|
16
16
|
signUp(email: string, password: string): Promise<SignInResult>;
|
|
17
17
|
signOut(): Promise<void>;
|
|
18
|
+
/** Edit the signed-in user's OWN name/avatar (EC-240 H-tail); refreshes the session. */
|
|
19
|
+
updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult>;
|
|
20
|
+
/** Change the signed-in user's OWN password (EC-240 H-tail); provider-delegated. */
|
|
21
|
+
changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult>;
|
|
18
22
|
}
|
|
19
23
|
declare const useAuth: () => AuthStore;
|
|
20
24
|
export interface AuthProviderProps {
|
|
@@ -46,6 +46,15 @@ export function AuthProvider({ children, adapter }) {
|
|
|
46
46
|
await auth.signOut();
|
|
47
47
|
apply(null);
|
|
48
48
|
},
|
|
49
|
+
updateProfile: async (input) => {
|
|
50
|
+
const result = await auth.updateProfile(input);
|
|
51
|
+
// A successful identity edit refreshes the session so the shell (and the
|
|
52
|
+
// account surface) reflect the new name immediately.
|
|
53
|
+
if (result.ok)
|
|
54
|
+
apply(result.session);
|
|
55
|
+
return result;
|
|
56
|
+
},
|
|
57
|
+
changePassword: (input) => auth.changePassword(input),
|
|
49
58
|
}), [auth, status, session, apply]);
|
|
50
59
|
return _jsx(AuthContext.Provider, { value: store, children: children });
|
|
51
60
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-context.js","sourceRoot":"","sources":["../../src/components/auth-context.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAG7E,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAA;
|
|
1
|
+
{"version":3,"file":"auth-context.js","sourceRoot":"","sources":["../../src/components/auth-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useContext, useEffect, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { ProjectRole } from '@elytracms/core/persistence'\nimport { selectAuthAdapter, sessionPrincipal } from '@elytracms/studio/lib/auth'\nimport type {\n AuthAdapter,\n AuthSession,\n AuthStatus,\n ChangePasswordInput,\n ChangePasswordResult,\n SignInResult,\n UpdateProfileInput,\n UpdateProfileResult,\n} from '@elytracms/studio/lib/auth'\nimport { setStudioSession } from '@elytracms/studio/lib/operations'\nimport { useOperationQuery } from '@elytracms/studio/lib/query'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\n\n/**\n * Session store (EC-150). Owns the one `AuthAdapter` the app talks to, keeps\n * the session in React state, and binds/unbinds the operations-layer principal\n * (`setStudioSession`) so every studio mutation runs through the role hook as\n * the signed-in user. Auth state machine: `loading` (restoring) →\n * `signed-in` | `signed-out` — all three are deterministic, renderable states.\n */\n\nexport interface AuthStore {\n status: AuthStatus\n session: AuthSession | null\n signIn(email: string, password: string): Promise<SignInResult>\n /** Register the first/new user on the active backend, then sign in (EC-179). */\n signUp(email: string, password: string): Promise<SignInResult>\n signOut(): Promise<void>\n /** Edit the signed-in user's OWN name/avatar (EC-240 H-tail); refreshes the session. */\n updateProfile(input: UpdateProfileInput): Promise<UpdateProfileResult>\n /** Change the signed-in user's OWN password (EC-240 H-tail); provider-delegated. */\n changePassword(input: ChangePasswordInput): Promise<ChangePasswordResult>\n}\n\nconst { Context: AuthContext, useRequired: useAuth } = createRequiredContext<AuthStore>('Auth', {\n article: 'an',\n})\n\nexport interface AuthProviderProps {\n children: ReactNode\n /** Test seam: inject an adapter; defaults to `selectAuthAdapter()`. */\n adapter?: AuthAdapter\n}\n\nexport function AuthProvider({ children, adapter }: AuthProviderProps) {\n const [auth] = useState<AuthAdapter>(() => adapter ?? selectAuthAdapter())\n const [status, setStatus] = useState<AuthStatus>('loading')\n const [session, setSession] = useState<AuthSession | null>(null)\n\n const apply = useCallback((next: AuthSession | null) => {\n setStudioSession(next ? sessionPrincipal(next) : null)\n setSession(next)\n setStatus(next ? 'signed-in' : 'signed-out')\n }, [])\n\n // Restore the session once on the client (SSR renders the 'loading' state).\n useEffect(() => {\n let cancelled = false\n void auth.currentSession().then((restored) => {\n if (!cancelled) apply(restored)\n })\n return () => {\n cancelled = true\n }\n }, [auth, apply])\n\n const store = useMemo<AuthStore>(\n () => ({\n status,\n session,\n signIn: async (email, password) => {\n const result = await auth.signIn({ email, password })\n if (result.ok) apply(result.session)\n return result\n },\n signUp: async (email, password) => {\n const result = await auth.signUp({ email, password })\n if (result.ok) apply(result.session)\n return result\n },\n signOut: async () => {\n await auth.signOut()\n apply(null)\n },\n updateProfile: async (input) => {\n const result = await auth.updateProfile(input)\n // A successful identity edit refreshes the session so the shell (and the\n // account surface) reflect the new name immediately.\n if (result.ok) apply(result.session)\n return result\n },\n changePassword: (input) => auth.changePassword(input),\n }),\n [auth, status, session, apply],\n )\n\n return <AuthContext.Provider value={store}>{children}</AuthContext.Provider>\n}\n\nexport { useAuth }\n\n/**\n * Like {@link useAuth}, but tolerant of provider-less renders. For shared\n * leaf surfaces (e.g. the operation-error banner's \"Sign in again\" action)\n * that are also rendered standalone in tests/styleguides: they degrade\n * gracefully instead of demanding the full provider tree.\n */\nexport function useOptionalAuth(): AuthStore | null {\n return useContext(AuthContext)\n}\n\n/**\n * The signed-in user's role on a project (for the shell's role badge).\n * Read through the one operations-backed query cache (EC-308/AD-17) via the\n * `members.find` query — no longer reaching past the spine into\n * `adapter.members` — and re-read when membership changes. `null` while loading\n * or when the user is not a member.\n */\nexport function useProjectRole(projectId: string | null): ProjectRole | null {\n const { session } = useAuth()\n const userId = session?.user.id ?? null\n const query = useOperationQuery<ProjectRole | null>({\n queryKey: ['projectRole', projectId ?? '∅', userId ?? '∅'],\n loader: async (operations) => {\n const result = await operations.members.find({\n projectId: projectId as string,\n userId: userId as string,\n })\n return result.ok ? (result.value?.role ?? null) : null\n },\n invalidateOn: ['members'],\n enabled: projectId !== null && userId !== null,\n })\n return query.data ?? null\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig, RestoreScope } from '@elytracms/core/cms-core';
|
|
2
|
+
import type { CmsDocument, CmsValidationIssue, CollectionDef, CollectionRegistry, DocumentRef, FieldDef, Locale, LocaleConfig, LocalizedString, RestoreScope } from '@elytracms/core/cms-core';
|
|
3
3
|
import type { DocumentVersionRecord } from '@elytracms/core/persistence';
|
|
4
4
|
import type { OperationError } from '@elytracms/studio/lib/operations';
|
|
5
5
|
import type { TableViewState } from '@elytracms/studio/lib/cms-views';
|
|
@@ -14,7 +14,12 @@ import type { BulkAction, BulkReport, BulkSetField } from './bulk-actions';
|
|
|
14
14
|
*/
|
|
15
15
|
export interface SavedView {
|
|
16
16
|
id: string;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The view label — a {@link LocalizedString} (EC-343): the built-in "All" default is
|
|
19
|
+
* a plain translated string; a config view's name may be a `{ locale }` map, resolved
|
|
20
|
+
* at the render site (`ViewTabs`) via the same `resolveLabel` path as `form.label`.
|
|
21
|
+
*/
|
|
22
|
+
name: LocalizedString;
|
|
18
23
|
state: TableViewState;
|
|
19
24
|
}
|
|
20
25
|
export interface ImportReport {
|
|
@@ -9,7 +9,7 @@ import { validateComposition, validateRichTextEmbeds } from './cms-composition-v
|
|
|
9
9
|
import { matchesRef, removeDoc, replaceDoc, upsertDoc } from './document-ref-utils';
|
|
10
10
|
import { createRequiredContext } from '@elytracms/studio/lib/required-context';
|
|
11
11
|
import { createCmsPersister, getStudioOperations } from '@elytracms/studio/lib/operations';
|
|
12
|
-
import { ALL_VIEW_ID, canonicalViewState, configViewsFor, defaultViewState, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
|
|
12
|
+
import { ALL_VIEW_ID, canonicalViewState, configViewIssues, configViewsFor, defaultConfigViewId, defaultViewState, viewStatesEqual, } from '@elytracms/studio/lib/cms-views';
|
|
13
13
|
import { useOptionalAssets } from '../assets/assets-store';
|
|
14
14
|
import { consumeStagedDocuments } from './document-staging';
|
|
15
15
|
import { applyCellValue, commitCellEdit } from './cell-edit';
|
|
@@ -63,6 +63,24 @@ export function buildNewDocument(collectionId, registry, seq, existingDocs) {
|
|
|
63
63
|
export function hasDocumentFor(docs, collectionId) {
|
|
64
64
|
return docs.some((d) => d.collection === collectionId);
|
|
65
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Surface a duplicate `default: true` across a collection's config views (EC-343) as
|
|
68
|
+
* a visible, one-time console warning — never a silent swallow. This is a developer
|
|
69
|
+
* config mistake (editors can't fix config-as-code), so it takes the same warn-once
|
|
70
|
+
* console channel the i18n runtime uses for config problems, not an editor banner;
|
|
71
|
+
* the first-declared default still seeds deterministically. Deduped per collection id.
|
|
72
|
+
*/
|
|
73
|
+
const warnedDuplicateDefaultCollections = new Set();
|
|
74
|
+
function warnDuplicateDefaultViews(collection) {
|
|
75
|
+
if (warnedDuplicateDefaultCollections.has(collection.id))
|
|
76
|
+
return;
|
|
77
|
+
const issues = configViewIssues(collection);
|
|
78
|
+
if (issues.length === 0)
|
|
79
|
+
return;
|
|
80
|
+
warnedDuplicateDefaultCollections.add(collection.id);
|
|
81
|
+
if (typeof console !== 'undefined')
|
|
82
|
+
console.warn(`[views] ${issues[0].message}`);
|
|
83
|
+
}
|
|
66
84
|
/** The unnamed built-in view: no filters, no sort, every field visible. */
|
|
67
85
|
function defaultView(collection) {
|
|
68
86
|
return {
|
|
@@ -441,11 +459,23 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
441
459
|
...(configCollection ? configViewsFor(configCollection) : []),
|
|
442
460
|
];
|
|
443
461
|
}, [registry]);
|
|
462
|
+
// The INITIAL active view for a collection (EC-343): a config view flagged
|
|
463
|
+
// `default: true` seeds the first-load lens; else the built-in "All" default. Only
|
|
464
|
+
// consulted before the editor has picked a view (`activeViews[id]` wins once set),
|
|
465
|
+
// so transient switching + Reset are unaffected. A duplicate `default` is warned
|
|
466
|
+
// once (never silent) and the first-declared default seeds deterministically.
|
|
467
|
+
const initialViewId = useCallback((collectionId) => {
|
|
468
|
+
const configCollection = resolveDeploymentCollections().find((c) => c.id === collectionId);
|
|
469
|
+
if (!configCollection)
|
|
470
|
+
return ALL_VIEW_ID;
|
|
471
|
+
warnDuplicateDefaultViews(configCollection);
|
|
472
|
+
return defaultConfigViewId(configCollection) ?? ALL_VIEW_ID;
|
|
473
|
+
}, []);
|
|
444
474
|
const activeViewOf = useCallback((collectionId) => {
|
|
445
|
-
const id = activeViews[collectionId] ??
|
|
475
|
+
const id = activeViews[collectionId] ?? initialViewId(collectionId);
|
|
446
476
|
return (viewsOf(collectionId).find((view) => view.id === id) ??
|
|
447
477
|
defaultView(registry.get(collectionId)));
|
|
448
|
-
}, [activeViews, viewsOf, registry]);
|
|
478
|
+
}, [activeViews, initialViewId, viewsOf, registry]);
|
|
449
479
|
const tableStateOf = useCallback((collectionId) => tableStates[collectionId] ?? activeViewOf(collectionId).state, [tableStates, activeViewOf]);
|
|
450
480
|
const dropTableState = useCallback((collectionId) => {
|
|
451
481
|
setTableStates((prev) => {
|
|
@@ -565,7 +595,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
565
595
|
validate,
|
|
566
596
|
isFieldReadOnly,
|
|
567
597
|
viewsOf,
|
|
568
|
-
activeViewId: (collectionId) => activeViews[collectionId] ??
|
|
598
|
+
activeViewId: (collectionId) => activeViews[collectionId] ?? initialViewId(collectionId),
|
|
569
599
|
setActiveView,
|
|
570
600
|
tableStateOf,
|
|
571
601
|
setTableState: (collectionId, update) => setTableStates((prev) => {
|
|
@@ -617,6 +647,7 @@ export function CmsProvider({ children, projectId, initialCollections, initialDo
|
|
|
617
647
|
viewsOf,
|
|
618
648
|
activeViews,
|
|
619
649
|
activeViewOf,
|
|
650
|
+
initialViewId,
|
|
620
651
|
tableStates,
|
|
621
652
|
tableStateOf,
|
|
622
653
|
setActiveView,
|