rimecms 1.0.0 → 1.0.1

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.
Files changed (68) hide show
  1. package/dist/adapter-sqlite/generate-schema/templates.server.d.ts +9 -9
  2. package/dist/adapter-sqlite/generate-schema/templates.server.js +9 -9
  3. package/dist/adapter-sqlite/paths.server.js +24 -15
  4. package/dist/adapter-sqlite/rows.server.d.ts +1 -1
  5. package/dist/core/adapter.d.ts +8 -4
  6. package/dist/core/boot.server.d.ts +2 -2
  7. package/dist/core/config/context.server.d.ts +2 -2
  8. package/dist/core/dev/codegen/routes/common.server.d.ts +2 -0
  9. package/dist/core/dev/codegen/routes/common.server.js +7 -3
  10. package/dist/core/dev/codegen/routes/util.server.d.ts +2 -1
  11. package/dist/core/dev/codegen/routes/util.server.js +4 -1
  12. package/dist/core/dev/config-memo.server.js +4 -0
  13. package/dist/core/prototype/collection/operations/duplicate.js +1 -1
  14. package/dist/core/prototype/collection/thumbnail/hooks/set-document-thumbnail.server.js +1 -1
  15. package/dist/core/prototype/shared/title/hooks/set-document-title.server.js +1 -1
  16. package/dist/core/prototype/shared/url/addresses.server.d.ts +10 -0
  17. package/dist/core/prototype/shared/url/addresses.server.js +17 -17
  18. package/dist/core/prototype/shared/url/hooks/detach-paths.server.d.ts +3 -2
  19. package/dist/core/prototype/shared/url/hooks/detach-paths.server.js +9 -8
  20. package/dist/core/prototype/shared/url/hooks/sync-paths.server.d.ts +3 -2
  21. package/dist/core/prototype/shared/url/hooks/sync-paths.server.js +5 -4
  22. package/dist/fields/blocks/component/focus/BlocksFocus.svelte +1 -1
  23. package/dist/fields/link/index.d.ts +1 -1
  24. package/dist/fields/relation/component/RelationInline.svelte +1 -1
  25. package/dist/fields/relation/index.d.ts +1 -41
  26. package/dist/fields/relation/index.js +0 -67
  27. package/dist/fields/relation/util.d.ts +69 -0
  28. package/dist/fields/relation/util.js +103 -0
  29. package/dist/fields/rich-text/client.d.ts +2 -5
  30. package/dist/fields/rich-text/client.js +1 -3
  31. package/dist/fields/rich-text/component/Cell.svelte +1 -1
  32. package/dist/fields/rich-text/core/features/resource/resource-extension.js +3 -3
  33. package/dist/fields/rich-text/core/features/resource/resource.svelte +1 -1
  34. package/dist/fields/rich-text/core/features/upload/upload-extension.js +3 -4
  35. package/dist/fields/rich-text/core/features/upload/upload.svelte +1 -1
  36. package/dist/fields/rich-text/index.d.ts +0 -17
  37. package/dist/fields/rich-text/index.js +0 -44
  38. package/dist/fields/rich-text/json.d.ts +18 -0
  39. package/dist/fields/rich-text/json.js +44 -0
  40. package/dist/index.d.ts +2 -9
  41. package/dist/index.js +3 -8
  42. package/dist/{panel/components/sections/live → live}/Consumer.svelte +2 -2
  43. package/dist/{panel/components/sections/live → live}/Consumer.svelte.d.ts +1 -1
  44. package/dist/{panel/components/sections/live → live}/LiveEdit.svelte +2 -2
  45. package/dist/{panel/components/sections/live → live}/Provider.svelte +1 -1
  46. package/dist/{panel/context/live.svelte.d.ts → live/context.svelte.d.ts} +1 -1
  47. package/dist/{panel/context/live.svelte.js → live/context.svelte.js} +3 -3
  48. package/dist/panel/components/sections/document/upload-header/drop-zone/DropZone.svelte.d.ts +1 -1
  49. package/dist/panel/components/ui/calendar/calendar.svelte.d.ts +1 -1
  50. package/dist/panel/components/ui/checkbox/checkbox.svelte.d.ts +1 -1
  51. package/dist/panel/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte.d.ts +1 -1
  52. package/dist/panel/components/ui/switch/switch.svelte.d.ts +1 -1
  53. package/dist/panel/context/collection.svelte.d.ts +2 -2
  54. package/dist/panel/context/config.svelte.d.ts +2 -2
  55. package/dist/panel/context/documentForm.svelte.js +1 -1
  56. package/dist/panel/index.d.ts +2 -5
  57. package/dist/panel/index.js +3 -6
  58. package/dist/panel/public.d.ts +4 -0
  59. package/dist/panel/public.js +6 -0
  60. package/dist/public.d.ts +13 -0
  61. package/dist/public.js +12 -0
  62. package/package.json +12 -6
  63. package/dist/fields/relation/client.d.ts +0 -28
  64. package/dist/fields/relation/client.js +0 -37
  65. /package/dist/{panel/util → fields/relation}/populate.d.ts +0 -0
  66. /package/dist/{panel/util → fields/relation}/populate.js +0 -0
  67. /package/dist/{panel/components/sections/live → live}/LiveEdit.svelte.d.ts +0 -0
  68. /package/dist/{panel/components/sections/live → live}/Provider.svelte.d.ts +0 -0
@@ -1,5 +1,5 @@
1
- import type { FieldReference } from '../../core/fields/builders/form-field-builder.js';
2
1
  import type { ColumnDeclaration, TableDeclaration } from '../../core/adapter.js';
2
+ import type { FieldReference } from '../../core/fields/builders/form-field-builder.js';
3
3
  import { type TableName } from '../naming.server.js';
4
4
  /**
5
5
  * Generates the standard imports needed for Drizzle ORM schema definitions
@@ -113,10 +113,11 @@ export declare const templateUniqueRequired: (field: {
113
113
  export declare const templatePathsTable: (owner: TableName) => string;
114
114
  /** Template rows Relation */
115
115
  /**
116
- * The whole schema's relations, in one declaration.
116
+ * The drizzle relations of the whole schema. A read lists them in its `with` to load related rows
117
+ * in the same query, by the names given here.
117
118
  *
118
- * Every relation rime generates is the same shape — a child row points at its owner through
119
- * `ownerId` — so the tree is `parent -> children` and both directions fall out of it:
119
+ * A child table (blocks, tree…) points at its owner through `ownerId`. It gets a relation each way,
120
+ * named after the table on the other end:
120
121
  *
121
122
  * ```ts
122
123
  * export const relations = defineRelations(tables, (r) => ({
@@ -131,14 +132,13 @@ export declare const templatePathsTable: (owner: TableName) => string;
131
132
  * }));
132
133
  * ```
133
134
  *
134
- * A relation is keyed by the table on the other end, which is what `buildWithParam` names when it
135
- * builds a `with`.
136
- *
137
- * A reference the read resolves is the other shape: the owner's row points at its target through
138
- * the field's own column, one direction, keyed by `joinName(column)`:
135
+ * A reference field that stores another document's id in its own column: `updatedBy` holds a staff id.
136
+ * It gets one relation, from that column to the target, named after the column. Two columns can
137
+ * point at the same table:
139
138
  *
140
139
  * ```ts
141
140
  * pages: {
141
+ * createdBy__$doc: r.one.staff({ from: r.pages.createdBy, to: r.staff.id }),
142
142
  * updatedBy__$doc: r.one.staff({ from: r.pages.updatedBy, to: r.staff.id })
143
143
  * }
144
144
  * ```
@@ -1,6 +1,6 @@
1
1
  import { toSnakeCase } from '../../util/string.js';
2
- import { baseTableName, declaredTableProperty, getSchemaColumnNames, joinName, tableName } from '../naming.server.js';
3
2
  import dedent from 'dedent';
3
+ import { baseTableName, declaredTableProperty, getSchemaColumnNames, joinName, tableName } from '../naming.server.js';
4
4
  const s = toSnakeCase;
5
5
  /**
6
6
  * Generates the standard imports needed for Drizzle ORM schema definitions
@@ -204,10 +204,11 @@ export const ${table} = sqliteTable('${table}', {
204
204
  };
205
205
  /** Template rows Relation */
206
206
  /**
207
- * The whole schema's relations, in one declaration.
207
+ * The drizzle relations of the whole schema. A read lists them in its `with` to load related rows
208
+ * in the same query, by the names given here.
208
209
  *
209
- * Every relation rime generates is the same shape — a child row points at its owner through
210
- * `ownerId` — so the tree is `parent -> children` and both directions fall out of it:
210
+ * A child table (blocks, tree…) points at its owner through `ownerId`. It gets a relation each way,
211
+ * named after the table on the other end:
211
212
  *
212
213
  * ```ts
213
214
  * export const relations = defineRelations(tables, (r) => ({
@@ -222,14 +223,13 @@ export const ${table} = sqliteTable('${table}', {
222
223
  * }));
223
224
  * ```
224
225
  *
225
- * A relation is keyed by the table on the other end, which is what `buildWithParam` names when it
226
- * builds a `with`.
227
- *
228
- * A reference the read resolves is the other shape: the owner's row points at its target through
229
- * the field's own column, one direction, keyed by `joinName(column)`:
226
+ * A reference field that stores another document's id in its own column: `updatedBy` holds a staff id.
227
+ * It gets one relation, from that column to the target, named after the column. Two columns can
228
+ * point at the same table:
230
229
  *
231
230
  * ```ts
232
231
  * pages: {
232
+ * createdBy__$doc: r.one.staff({ from: r.pages.createdBy, to: r.staff.id }),
233
233
  * updatedBy__$doc: r.one.staff({ from: r.pages.updatedBy, to: r.staff.id })
234
234
  * }
235
235
  * ```
@@ -56,28 +56,37 @@ const createPathsHandle = ({ db, tables }) => {
56
56
  .returning();
57
57
  return row;
58
58
  },
59
- // One statement for every locale: each row takes its new parent's path in its own locale.
60
- moveUnder: async ({ slug, ownerId, parentId }) => {
59
+ // One statement per locale, in one transaction: each row takes its new parent's path in its
60
+ // locale, and its slug there.
61
+ moveUnder: async ({ slug, ownerId, parentId, slugs }) => {
61
62
  const { paths } = tablesOf(slug);
62
- const moved = await db
63
- .update(paths)
64
- .set({ parentPath: parentId ? parentPathIn(paths, parentId, paths.locale) : null })
65
- .where(eq(paths.ownerId, ownerId))
66
- .returning();
67
- return rowsOf(moved);
63
+ return db.transaction(async (tx) => {
64
+ const moved = [];
65
+ for (const [locale, value] of Object.entries(slugs)) {
66
+ const rows = await tx
67
+ .update(paths)
68
+ .set({
69
+ slug: value,
70
+ parentPath: parentId ? parentPathIn(paths, parentId, locale) : null
71
+ })
72
+ .where(and(eq(paths.ownerId, ownerId), eq(paths.locale, locale)))
73
+ .returning();
74
+ moved.push(...rows);
75
+ }
76
+ return rowsOf(moved);
77
+ });
68
78
  },
69
- detachChildren: async ({ slug, ownerId }) => {
79
+ children: async ({ slug, ownerId }) => {
70
80
  const { paths } = tablesOf(slug);
71
81
  const self = alias(paths, 'self');
72
- const detached = await db
73
- .update(paths)
74
- .set({ parentPath: null })
82
+ const rows = await db
83
+ .selectDistinct({ ownerId: paths.ownerId })
84
+ .from(paths)
75
85
  .where(sql `(${paths.locale}, ${paths.parentPath}) IN (${db
76
86
  .select({ locale: self.locale, path: self.path })
77
87
  .from(self)
78
- .where(eq(self.ownerId, ownerId))})`)
79
- .returning();
80
- return rowsOf(detached);
88
+ .where(eq(self.ownerId, ownerId))})`);
89
+ return rows.map((row) => row.ownerId);
81
90
  },
82
91
  // `path/` <= p < `path0` is every path starting with `path/`, read off the unique index:
83
92
  // '0' is the character after '/'.
@@ -30,7 +30,7 @@ export declare const bundles: ({ db, tables, configCtx, config, versions }: Hand
30
30
  };
31
31
  self: {
32
32
  slug: never;
33
- config: BuiltCollection | BuiltArea;
33
+ config: BuiltArea | BuiltCollection;
34
34
  versions: VersionsTable | undefined;
35
35
  };
36
36
  };
@@ -330,17 +330,21 @@ export interface PathsHandle {
330
330
  locale: string;
331
331
  value: string;
332
332
  }): Promise<PathRow | undefined>;
333
- /** Under another parent, or at the top, in every locale at once. Answers the page's rows. */
333
+ /**
334
+ * Under another parent, or at the top, with the slug it takes there in each of its locales:
335
+ * `{ en: 'team', fr: 'equipe' }`. Answers the page's rows.
336
+ */
334
337
  moveUnder(args: {
335
338
  slug: PrototypeSlug;
336
339
  ownerId: string;
337
340
  parentId: string | null;
341
+ slugs: Record<string, string>;
338
342
  }): Promise<PathRow[]>;
339
- /** Its children go to the top, before it is deleted. Answers their rows. */
340
- detachChildren(args: {
343
+ /** The pages directly under this one, in any locale. */
344
+ children(args: {
341
345
  slug: PrototypeSlug;
342
346
  ownerId: string;
343
- }): Promise<PathRow[]>;
347
+ }): Promise<string[]>;
344
348
  /** The row at `path` and, with `withDescendants`, every row below it, in one locale. */
345
349
  under(args: {
346
350
  slug: PrototypeSlug;
@@ -22,13 +22,13 @@ export declare const bootRime: <const C extends Config>(config: BuildConfig<C>)
22
22
  } ? { [K in NonNullable<C["areas"]>[number] as K extends {
23
23
  slug: infer N;
24
24
  } ? N extends string ? N : never : never]: K; } : Record<string, never>;
25
- readonly prototypes: (import("./config/types.js").BuiltCollection | import("./config/types.js").BuiltArea)[];
25
+ readonly prototypes: (import("./config/types.js").BuiltArea | import("./config/types.js").BuiltCollection)[];
26
26
  getDefaultLocale(): string | undefined;
27
27
  getLocalesCodes: () => string[];
28
28
  isValidLocale: (locale: string) => boolean;
29
29
  getArea: (slug: string) => import("./config/types.js").BuiltArea;
30
30
  getCollection: (slug: string) => import("./config/types.js").BuiltCollection;
31
- getBySlug: (slug: string) => import("./config/types.js").BuiltCollection | import("./config/types.js").BuiltArea;
31
+ getBySlug: (slug: string) => import("./config/types.js").BuiltArea | import("./config/types.js").BuiltCollection;
32
32
  isCollection: (slug?: string) => slug is import("./prototype/types.js").CollectionSlug;
33
33
  isArea: (slug?: string) => slug is import("./prototype/types.js").AreaSlug;
34
34
  getDocumentPrototype(slug: import("./prototype/types.js").PrototypeSlug): "area" | "collection";
@@ -42,7 +42,7 @@ export declare function createConfigContext<const C extends Config>(config: Buil
42
42
  /**
43
43
  * Gets every built prototype config, whatever its kind
44
44
  */
45
- readonly prototypes: (import("../../types.js").BuiltCollection | import("../../types.js").BuiltArea)[];
45
+ readonly prototypes: (import("../../types.js").BuiltArea | import("../../types.js").BuiltCollection)[];
46
46
  /**
47
47
  * Gets the default locale from the configuration
48
48
  */
@@ -66,7 +66,7 @@ export declare function createConfigContext<const C extends Config>(config: Buil
66
66
  /**
67
67
  * Retrieves either an area or collection configuration by its slug
68
68
  */
69
- getBySlug: (slug: string) => import("../../types.js").BuiltCollection | import("../../types.js").BuiltArea;
69
+ getBySlug: (slug: string) => import("../../types.js").BuiltArea | import("../../types.js").BuiltCollection;
70
70
  /**
71
71
  * Checks if a slug represents a collection
72
72
  */
@@ -33,3 +33,5 @@ declare const paramMatcher: (slugs: string[]) => string;
33
33
  declare const customRoute: (config: any) => string;
34
34
  export declare const commonRoutes: Routes;
35
35
  export { customRoute, paramMatcher, prototypeApiServer };
36
+ /** Every fixed route file as the templates write it: a template that changes regenerates them. */
37
+ export declare const templatesSurface: () => string;
@@ -90,7 +90,7 @@ export const load = async ({ locals }: ServerLoadEvent) => {
90
90
  */
91
91
  const signInPage = () => `
92
92
  <script>
93
- import { SignIn } from '${PACKAGE_NAME}/panel';
93
+ import { SignIn } from '${PACKAGE_NAME}/panel/public';
94
94
  const { data } = $props();
95
95
  </script>
96
96
 
@@ -113,7 +113,7 @@ export const actions = {
113
113
  */
114
114
  const forgotPasswordPage = () => `
115
115
  <script>
116
- import { ForgotPassword } from '${PACKAGE_NAME}/panel'
116
+ import { ForgotPassword } from '${PACKAGE_NAME}/panel/public'
117
117
  const { data } = $props();
118
118
  </script>
119
119
  <ForgotPassword {data} />`;
@@ -132,7 +132,7 @@ export const load = (event: ServerLoadEvent) => event.locals.routes.panel.load.f
132
132
  */
133
133
  const resetPasswordPage = () => `
134
134
  <script>
135
- import { ResetPassword } from '${PACKAGE_NAME}/panel';
135
+ import { ResetPassword } from '${PACKAGE_NAME}/panel/public';
136
136
  const { data } = $props();
137
137
  </script>
138
138
 
@@ -435,3 +435,7 @@ export const commonRoutes = {
435
435
  }
436
436
  };
437
437
  export { customRoute, paramMatcher, prototypeApiServer };
438
+ /** Every fixed route file as the templates write it: a template that changes regenerates them. */
439
+ export const templatesSurface = () => Object.entries(commonRoutes)
440
+ .flatMap(([pattern, files]) => Object.entries(files).map(([type, render]) => `${pattern}/${type}:${render()}`))
441
+ .join('\n');
@@ -16,7 +16,8 @@ export type Routes = Record<string, RouteDefinition>;
16
16
  * custom, css written straight from config.panel
17
17
  * prototypes the slug lists the [slug=<name>] matchers accept
18
18
  * rest one +server.ts per prototype route path, and the methods it exports
19
- * version the templates every file above is written from ship with the package
19
+ * templates the fixed files as the package's templates write them today
20
+ * version the package the templates ship with
20
21
  * ```
21
22
  *
22
23
  * `PANEL_ROUTE` and `CONFIG_DIR` are read once at process start, so a changed value only takes
@@ -6,6 +6,7 @@ import { PANEL_ROUTE } from '../../../routes/constants.server.js';
6
6
  import { slugify } from '../../../../util/string.js';
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
+ import { templatesSurface } from './common.server.js';
9
10
  /**
10
11
  * Whether the generated route tree still matches what the config and the package would produce.
11
12
  *
@@ -17,7 +18,8 @@ import path from 'path';
17
18
  * custom, css written straight from config.panel
18
19
  * prototypes the slug lists the [slug=<name>] matchers accept
19
20
  * rest one +server.ts per prototype route path, and the methods it exports
20
- * version the templates every file above is written from ship with the package
21
+ * templates the fixed files as the package's templates write them today
22
+ * version the package the templates ship with
21
23
  * ```
22
24
  *
23
25
  * `PANEL_ROUTE` and `CONFIG_DIR` are read once at process start, so a changed value only takes
@@ -39,6 +41,7 @@ export function shouldRegenerateRoutes(config) {
39
41
  .map((p) => `${p.type}:${p.slug}`)
40
42
  .join(',')}
41
43
  rest:${restSurface()}
44
+ templates:${templatesSurface()}
42
45
  version:${rimeVersion()}
43
46
  `;
44
47
  if (cache.matches('routes', memo))
@@ -2,6 +2,7 @@ import { flatten } from 'flat';
2
2
  import cache from './cache.server.js';
3
3
  import { CONFIG_DIR } from './constants.server.js';
4
4
  import { restSurface } from './prototype-routes.server.js';
5
+ import { templatesSurface } from './codegen/routes/common.server.js';
5
6
  import { rimeVersion } from './version.server.js';
6
7
  /**
7
8
  * We actually need to serialize config values that will trigger
@@ -76,6 +77,9 @@ const writeMemo = (config) => {
76
77
  // `generateRoutes` included, so a route added to a prototype has to move it — a version bump
77
78
  // does not, inside this repo, and the config never mentions those paths at all.
78
79
  .concat(`REST_SURFACE:${restSurface()}`)
80
+ // Nor are the route templates, which change inside this repo and between two packs of one
81
+ // version: the routes they write must follow.
82
+ .concat(`ROUTE_TEMPLATES:${templatesSurface()}`)
79
83
  .join('\n');
80
84
  if (cache.matches('config', memoStr))
81
85
  return false;
@@ -1,5 +1,5 @@
1
1
  import { copyLocales } from '../../../locale/copy.server.js';
2
- import { isJSONContent, richTextJSONToText } from '../../../../fields/rich-text/index.js';
2
+ import { isJSONContent, richTextJSONToText } from '../../../../fields/rich-text/json.js';
3
3
  import { getValueAtPath, isObjectLiteral, setValueAtPath } from '../../../../util/object.js';
4
4
  // If block is localized should not keep its id so it created a new one
5
5
  // If block is not localized than it should keep its id so block is updated
@@ -1,5 +1,5 @@
1
1
  import { Hooks } from '../../../../pipeline/define-hook.js';
2
- import { isRelationResolved } from '../../../../../fields/relation/index.js';
2
+ import { isRelationResolved } from '../../../../../fields/relation/util.js';
3
3
  import {} from '../../../../../types.js';
4
4
  import { getValueAtPath } from '../../../../../util/object.js';
5
5
  /**
@@ -1,5 +1,5 @@
1
1
  import { Hooks } from '../../../../pipeline/define-hook.js';
2
- import { richTextJSONToText } from '../../../../../fields/rich-text/index.js';
2
+ import { richTextJSONToText } from '../../../../../fields/rich-text/json.js';
3
3
  import { getValueAtPath, isObjectLiteral } from '../../../../../util/object.js';
4
4
  /**
5
5
  * Puts `title` on the document, read from whichever field `asTitle` resolved to.
@@ -9,6 +9,16 @@ export declare const addAddresses: (args: {
9
9
  config: BuiltCollection;
10
10
  ids: string[];
11
11
  }) => Promise<void>;
12
+ /**
13
+ * Moves a page and its subtree under `parentId`, or to the top when it is null. In each locale the
14
+ * page keeps its slug, or takes the next free one when a new sibling holds it: `team`, `team-2`.
15
+ */
16
+ export declare const movePage: (args: {
17
+ event: RequestEvent;
18
+ config: BuiltCollection;
19
+ ownerId: string;
20
+ parentId: string | null;
21
+ }) => Promise<any>;
12
22
  /**
13
23
  * The document with its address as it now is: the read that produced it ran before a create or an
14
24
  * update gave it one.
@@ -1,6 +1,4 @@
1
- import { walkFields } from '../../../fields/walk.js';
2
1
  import { logger } from '../../../logger.server.js';
3
- import { getValueAtPath } from '../../../../util/object.js';
4
2
  import { hasUrl } from './enabled.js';
5
3
  import { freeSlug, toSlug } from './format.server.js';
6
4
  import { formatRows, readForUrls } from './refresh.server.js';
@@ -9,20 +7,8 @@ export const addressLocales = (event) => {
9
7
  const codes = event.locals.rime.config.getLocalesCodes();
10
8
  return codes.length ? codes : [''];
11
9
  };
12
- /**
13
- * The slug a page starts with: the collection's own slug field when it has one, its title
14
- * otherwise, its id as a last resort.
15
- */
16
- const initialSlug = (config, doc) => {
17
- for (const { field, path } of walkFields(config.fields)) {
18
- if (field.type !== 'slug')
19
- continue;
20
- const value = toSlug(getValueAtPath(path, doc));
21
- if (value)
22
- return value;
23
- }
24
- return toSlug(doc.title) || doc.id;
25
- };
10
+ /** The slug a page starts with: its title, its id as a last resort. */
11
+ const initialSlug = (doc) => toSlug(doc.title) || doc.id;
26
12
  /**
27
13
  * Gives these pages their address in one locale: a slug free among their siblings, under their
28
14
  * parent, then their url.
@@ -43,7 +29,7 @@ const addAddressesIn = async (args) => {
43
29
  for (const id of ready) {
44
30
  const parentId = parentOf(id);
45
31
  const taken = await paths.siblingSlugs({ slug: config.slug, locale, parentId });
46
- const slug = freeSlug(initialSlug(config, docs.get(id)), taken);
32
+ const slug = freeSlug(initialSlug(docs.get(id)), taken);
47
33
  inserted.push(...(await paths.insert({
48
34
  slug: config.slug,
49
35
  ownerId: id,
@@ -70,6 +56,20 @@ export const addAddresses = async (args) => {
70
56
  await addAddressesIn({ event, config, ids: idsHere, locale });
71
57
  }
72
58
  };
59
+ /**
60
+ * Moves a page and its subtree under `parentId`, or to the top when it is null. In each locale the
61
+ * page keeps its slug, or takes the next free one when a new sibling holds it: `team`, `team-2`.
62
+ */
63
+ export const movePage = async (args) => {
64
+ const { event, config, ownerId, parentId } = args;
65
+ const { paths } = event.locals.rime.adapter;
66
+ const slugs = {};
67
+ for (const row of await paths.get({ slug: config.slug, ownerId })) {
68
+ const taken = await paths.siblingSlugs({ slug: config.slug, locale: row.locale, parentId });
69
+ slugs[row.locale] = freeSlug(row.slug, taken);
70
+ }
71
+ return paths.moveUnder({ slug: config.slug, ownerId, parentId, slugs });
72
+ };
73
73
  /**
74
74
  * The document with its address as it now is: the read that produced it ran before a create or an
75
75
  * update gave it one.
@@ -1,5 +1,6 @@
1
1
  /**
2
- * Before a delete, the page's children go to the top level with their subtrees, as `_parent`
3
- * does, and get their new urls. The page's own rows go with it by cascade.
2
+ * Before a delete, the page's children go to the top with their subtrees, as `_parent` does, and
3
+ * get their new urls. A child whose slug a top page holds takes the next free one. The page's own
4
+ * rows go with it by cascade.
4
5
  */
5
6
  export declare const detachPaths: import("../../../../pipeline/types.js").Hook<"generic", "delete", "before">;
@@ -1,18 +1,19 @@
1
1
  import { Hooks } from '../../../../pipeline/define-hook.js';
2
+ import { movePage } from '../addresses.server.js';
2
3
  import { refreshUrls } from '../refresh.server.js';
3
4
  /**
4
- * Before a delete, the page's children go to the top level with their subtrees, as `_parent`
5
- * does, and get their new urls. The page's own rows go with it by cascade.
5
+ * Before a delete, the page's children go to the top with their subtrees, as `_parent` does, and
6
+ * get their new urls. A child whose slug a top page holds takes the next free one. The page's own
7
+ * rows go with it by cascade.
6
8
  */
7
9
  export const detachPaths = Hooks.beforeDelete(async function detachPaths(args) {
8
10
  const { event, doc } = args;
9
11
  const config = args.config;
10
- const detached = await event.locals.rime.adapter.paths.detachChildren({
11
- slug: config.slug,
12
- ownerId: doc.id
13
- });
14
- for (const row of detached) {
15
- await refreshUrls({ event, config, locale: row.locale, path: row.path });
12
+ const { paths } = event.locals.rime.adapter;
13
+ for (const ownerId of await paths.children({ slug: config.slug, ownerId: doc.id })) {
14
+ for (const row of await movePage({ event, config, ownerId, parentId: null })) {
15
+ await refreshUrls({ event, config, locale: row.locale, path: row.path });
16
+ }
16
17
  }
17
18
  return args;
18
19
  });
@@ -1,6 +1,7 @@
1
1
  /**
2
- * After a save. A move carries the page and its subtree under the new parent, in every locale at
3
- * once. Any other save formats the page's own urls again, since `$url` may read any of its fields.
2
+ * After a save. A move carries the page and its subtree under the new parent, in every locale,
3
+ * with a free slug there. Any other save formats the page's own urls again, since `$url` may read
4
+ * any of its fields.
4
5
  *
5
6
  * An auto-save moves nothing and changes no url: it is skipped.
6
7
  */
@@ -1,10 +1,11 @@
1
1
  import { Hooks } from '../../../../pipeline/define-hook.js';
2
2
  import { isNested } from '../../../collection/nested/enabled.js';
3
- import { addAddresses, withAddress } from '../addresses.server.js';
3
+ import { addAddresses, movePage, withAddress } from '../addresses.server.js';
4
4
  import { refreshUrls } from '../refresh.server.js';
5
5
  /**
6
- * After a save. A move carries the page and its subtree under the new parent, in every locale at
7
- * once. Any other save formats the page's own urls again, since `$url` may read any of its fields.
6
+ * After a save. A move carries the page and its subtree under the new parent, in every locale,
7
+ * with a free slug there. Any other save formats the page's own urls again, since `$url` may read
8
+ * any of its fields.
8
9
  *
9
10
  * An auto-save moves nothing and changes no url: it is skipped.
10
11
  */
@@ -18,7 +19,7 @@ export const syncPaths = Hooks.afterUpdate(async function syncPaths(args) {
18
19
  const wasUnder = context.originalDoc?._parent ?? null;
19
20
  const moved = isNested(config) && parentId !== wasUnder;
20
21
  const rows = moved
21
- ? await paths.moveUnder({ slug: config.slug, ownerId: doc.id, parentId })
22
+ ? await movePage({ event, config, ownerId: doc.id, parentId })
22
23
  : await paths.get({ slug: config.slug, ownerId: doc.id });
23
24
  // A page saved before it had an address gets one.
24
25
  if (!rows.length) {
@@ -10,7 +10,7 @@
10
10
  import type { DocumentFormContext } from '../../../../panel/context/documentForm.svelte.js';
11
11
  import { getLocaleContext } from '../../../../panel/context/locale.svelte.js';
12
12
  import { getNavContext } from '../../../../panel/context/nav.svelte.js';
13
- import { populate } from '../../../../panel/util/populate.js';
13
+ import { populate } from '../../../relation/populate.js';
14
14
  import { capitalize } from '../../../../util/string.js';
15
15
  import { ChevronRight, PanelRight, ToyBrick, X } from '@lucide/svelte';
16
16
  import { untrack } from 'svelte';
@@ -56,7 +56,7 @@ export declare class LinkFieldBuilder extends FormFieldBuilder<LinkField> {
56
56
  root?: boolean | undefined;
57
57
  index?: boolean | undefined;
58
58
  layout: "compact" | "default";
59
- types: "tel"[] | "email"[] | "url"[] | "anchor"[];
59
+ types: "url"[] | "email"[] | "tel"[] | "anchor"[];
60
60
  } & {
61
61
  component: import("svelte").Component<any>;
62
62
  cell?: import("svelte").Component<{
@@ -9,7 +9,7 @@
9
9
  import { getConfigContext } from '../../../panel/context/config.svelte.js';
10
10
  import type { DocumentFormContext } from '../../../panel/context/documentForm.svelte.js';
11
11
  import { getLocaleContext } from '../../../panel/context/locale.svelte.js';
12
- import { populate } from '../../../panel/util/populate.js';
12
+ import { populate } from '../populate.js';
13
13
  import { uploadFiles, type UploadProgress } from '../../../panel/util/upload-file.js';
14
14
  import { Image as ImageIcon } from '@lucide/svelte';
15
15
  import type { Snippet } from 'svelte';
@@ -1,7 +1,7 @@
1
1
  import type { DataType } from '../../core/fields/builders/form-field-builder.js';
2
2
  import { FormFieldBuilder } from '../../core/fields/builders/form-field-builder.js';
3
3
  import type { CollectionSlug, GenericDoc } from '../../core/prototype/types.js';
4
- import type { DefaultValueFn, FormField, RelationRef, RelationValue } from '../types.js';
4
+ import type { DefaultValueFn, FormField } from '../types.js';
5
5
  import type { RegisterCollection } from '../../index.js';
6
6
  import type { WithOptional } from '../../util/types.js';
7
7
  export declare class RelationFieldBuilder<Doc extends GenericDoc = GenericDoc> extends FormFieldBuilder<RelationField<Doc>> {
@@ -29,46 +29,6 @@ export declare class RelationFieldBuilder<Doc extends GenericDoc = GenericDoc> e
29
29
  protected generateType(): string;
30
30
  }
31
31
  export declare const relation: (name: string) => RelationFieldBuilder<GenericDoc>;
32
- /**
33
- * Checks if a relation value is resolved (contains the actual referenced document).
34
- *
35
- * @example
36
- * // Returns true for a resolved relation
37
- * isRelationResolved({ title: 'Home Page', _prototype: 'collection', _type: 'pages' });
38
- */
39
- export declare const isRelationResolved: <T>(value: any) => value is T;
40
- /**
41
- * Checks if a relation value is a reference (contains only the relationTo and documentId).
42
- *
43
- * @example
44
- * // Returns true for a relation reference
45
- * isRelationRef({ relationTo: 'pages', documentId: '123' });
46
- */
47
- export declare const isRelationRef: (value: unknown) => value is RelationRef;
48
- /**
49
- * Checks if a relation value is unresolved (contains only reference information).
50
- *
51
- * @example
52
- * // Returns true for an unresolved relation
53
- * isRelationUnresolved({ relationTo: 'pages', documentId: '123' });
54
- */
55
- export declare const isRelationUnresolved: (value: any) => value is Omit<Relation, "path" | "position" | "ownerId">;
56
- /**
57
- * Resolves a relation reference to the actual document it points to.
58
- *
59
- * @example
60
- * // Resolves a relation reference to the actual document
61
- * const doc = await resolveRelationRef({ relationTo: 'pages', documentId: '123' });
62
- */
63
- export declare function resolveRelationRef<T>(item: T | RelationRef | string): Promise<T>;
64
- /**
65
- * Resolves a relation field value to the actual documents it points to.
66
- *
67
- * @example
68
- * // Resolves a relation value to the actual documents
69
- * const docs = await resolveRelation([{ relationTo: 'pages', documentId: '123' }]);
70
- */
71
- export declare function resolveRelation<T>(value: RelationValue<T> | null | undefined): Promise<T[] | null | undefined>;
72
32
  /****************************************************/
73
33
  export type RelationField<Doc extends GenericDoc = GenericDoc> = FormField & {
74
34
  type: 'relation';