Procházet zdrojové kódy

Merge branch 'master' into fix/10811-page-publish-unpublish-idor

Yuki Takei před 4 dny
rodič
revize
264037a907

+ 46 - 0
.claude/rules/github-cli.md

@@ -0,0 +1,46 @@
+# GitHub CLI (gh) Requirements
+
+## CRITICAL: gh CLI Authentication is Mandatory
+
+When any task requires GitHub operations (PRs, issues, releases, checks, etc.), you MUST use the `gh` CLI.
+
+**If `gh` CLI is not authenticated or not available, you MUST:**
+
+1. **STOP immediately** — do NOT attempt any fallback (WebFetch, curl, API calls, etc.)
+2. **Tell the user** that `gh` CLI authentication is required
+3. **Prompt the user** to run `gh auth login` before continuing
+4. **Wait** — do not proceed until the user confirms authentication
+
+## Prohibited Fallbacks
+
+The following fallbacks are **STRICTLY FORBIDDEN** when `gh` is unavailable or unauthenticated:
+
+- Using `WebFetch` to scrape GitHub URLs
+- Using `curl` against the GitHub API directly
+- Using `WebSearch` to find PR/issue information
+- Any other workaround that bypasses `gh` CLI
+
+## Required Check
+
+Before any `gh` command, if you are unsure about authentication status, run:
+
+```bash
+gh auth status
+```
+
+If the output indicates the user is not logged in, **STOP and prompt**:
+
+> `gh` CLI is not authenticated. Please run `gh auth login` and try again.
+
+## Example Correct Behavior
+
+```
+# gh not authenticated → STOP
+User: Please review PR #123
+Assistant: gh CLI is not authenticated. Please run `gh auth login` first, then retry.
+[Session stops — no fallback attempted]
+```
+
+## Why This Rule Exists
+
+Falling back to WebFetch or other HTTP-based access when `gh` is unavailable silently bypasses authentication and can expose unintended behavior. Stopping and prompting ensures credentials are properly configured before any GitHub interaction.

+ 3 - 1
.devcontainer/app/devcontainer.json

@@ -49,7 +49,9 @@
         "mongodb.mongodb-vscode",
         // Debug
         "msjsdiag.debugger-for-chrome",
-        "firefox-devtools.vscode-firefox-debug"
+        "firefox-devtools.vscode-firefox-debug",
+        // prisma
+        "Prisma.prisma@6.19.0"
       ],
       "settings": {
         "terminal.integrated.defaultProfile.linux": "bash"

+ 8 - 1
.github/workflows/reusable-app-prod.yml

@@ -69,16 +69,19 @@ jobs:
         tar -zcf production.tar.gz --exclude ./apps/app/.next/cache \
           package.json \
           node_modules \
+          tsconfig.base.json \
           apps/app/.next \
           apps/app/config \
           apps/app/dist \
+          apps/app/prisma \
           apps/app/public \
           apps/app/resource \
           apps/app/tmp \
           apps/app/.env.production* \
           apps/app/node_modules \
           apps/app/next.config.js \
-          apps/app/package.json
+          apps/app/package.json \
+          apps/app/tsconfig.json
         echo "file=production.tar.gz" >> $GITHUB_OUTPUT
 
     - name: Upload production files as artifact
@@ -124,6 +127,8 @@ jobs:
         - 9200/tcp
         env:
           discovery.type: single-node
+          # ES 9.x enables security (HTTPS + auth) by default; disable for plaintext CI access
+          xpack.security.enabled: false
 
     steps:
     - uses: actions/setup-node@v4
@@ -193,6 +198,8 @@ jobs:
         - 9200/tcp
         env:
           discovery.type: single-node
+          # ES 9.x enables security (HTTPS + auth) by default; disable for plaintext CI access
+          xpack.security.enabled: false
 
     steps:
     - uses: actions/checkout@v4

+ 4 - 0
.vscode/settings.json

@@ -21,6 +21,10 @@
     "editor.defaultFormatter": "biomejs.biome"
   },
 
+  "[prisma]": {
+    "editor.defaultFormatter": "Prisma.prisma"
+  },
+
   // use vscode-stylelint
   // see https://github.com/stylelint/vscode-stylelint
   "stylelint.validate": ["css", "less", "scss"],

+ 2 - 0
AGENTS.md

@@ -24,6 +24,8 @@ GROWI is a team collaboration wiki platform using Markdown, featuring hierarchic
 | **coding-style** | Coding conventions, naming, exports, immutability, comments |
 | **security** | Security checklist, secret management, OWASP vulnerability prevention |
 | **performance** | Model selection, context management, build troubleshooting |
+| **github-cli** | **CRITICAL**: gh CLI auth required; stop immediately if unauthenticated |
+
 | **testing** | Test commands, pnpm vitest usage |
 
 ### On-Demand Skills

+ 20 - 1
CHANGELOG.md

@@ -1,9 +1,28 @@
 # Changelog
 
-## [Unreleased](https://github.com/growilabs/compare/v7.5.1...HEAD)
+## [Unreleased](https://github.com/growilabs/compare/v7.5.2...HEAD)
 
 *Please do not manually update this file. We've automated the process.*
 
+## [v7.5.2](https://github.com/growilabs/compare/v7.5.1...v7.5.2) - 2026-04-22
+
+### 🚀 Improvement
+
+* imprv: Render page tree item title as a real anchor (#11029) @yuki-takei
+
+### 🐛 Bug Fixes
+
+* fix(editor): Editor breaks when an ID created by useId() is passed to a  reactstrap Popover/Tooltip targets (#11039) @yuki-takei
+* fix: Request requiring login is being made on the shared link page (#11026) @miya
+* fix: Search scope children as default setting is not applied (#11027) @miya
+* fix(slack): Normalize proxyUri to undefined for custom bot without proxy mode (#11035) @yuki-takei
+* fix: Avoid setState-in-render warning in useHydrateGlobalEachAtoms (#11023) @yuki-takei
+* fix: Stabilize watch-rendering-and-rescroll test suite (#11037) @yuki-takei
+
+### 🧰 Maintenance
+
+* support: Upgrade Biome (#11040) @yuki-takei
+
 ## [v7.5.1](https://github.com/growilabs/compare/v7.5.0...v7.5.1) - 2026-04-16
 
 ### 💎 Features

+ 23 - 0
apps/app/.claude/rules/ui-pitfalls.md

@@ -0,0 +1,23 @@
+# UI Pitfalls
+
+## useId() must not be passed to reactstrap `target` prop
+
+React's `useId()` generates IDs containing colons (`:r0:`, `:r1:`, `:r2:`). These are valid HTML `id` attributes but **invalid CSS selectors**.
+
+reactstrap's `findDOMElements()` resolves string targets via `document.querySelectorAll(target)`, which throws `DOMException: is not a valid selector` when the string contains colons.
+
+```tsx
+// ❌ WRONG: useId() output passed as string target
+const popoverTargetId = useId();
+<button id={popoverTargetId}>...</button>
+<Popover target={popoverTargetId} />  // → DOMException at componentDidMount
+
+// ✅ CORRECT: use ref — reactstrap resolves refs via .current, bypassing querySelectorAll
+const popoverTargetRef = useRef<HTMLButtonElement>(null);
+<button ref={popoverTargetRef}>...</button>
+<Popover target={popoverTargetRef} />
+```
+
+**Applies to all reactstrap components with a `target` prop**: `Popover`, `Tooltip`, `UncontrolledPopover`, `UncontrolledTooltip`, etc.
+
+**Safe uses of `useId()`**: `id=`, `htmlFor=`, `aria-labelledby=`, `aria-describedby=` — these use `getElementById` internally, which does not parse CSS.

+ 3 - 0
apps/app/.gitignore

@@ -23,3 +23,6 @@ next.config.js
 
 # cache
 /.swc/
+
+# prisma
+/src/generated/prisma

+ 1 - 1
apps/app/docker/README.md

@@ -10,7 +10,7 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 
-* [`7.5.1`, `7.4`, `7`, `latest` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.5.1/apps/app/docker/Dockerfile)
+* [`7.5.2`, `7.4`, `7`, `latest` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.5.2/apps/app/docker/Dockerfile)
 * [`7.3.0`, `7.3` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.3.0/apps/app/docker/Dockerfile)
 * [`7.2.0`, `7.2` (Dockerfile)](https://github.com/growilabs/growi/blob/v7.2.0/apps/app/docker/Dockerfile)
 

+ 17 - 5
apps/app/package.json

@@ -1,10 +1,11 @@
 {
   "name": "@growi/app",
-  "version": "7.5.2-RC.0",
+  "version": "7.5.3-RC.0",
   "license": "MIT",
   "private": true,
   "scripts": {
     "//// for production": "",
+    "postinstall": "prisma generate",
     "build": "run-p build:*",
     "start": "next start",
     "build:client": "next build",
@@ -16,16 +17,19 @@
     "preserver": "cross-env NODE_ENV=production pnpm run migrate",
     "pre:styles-commons": "vite build -c vite.vendor-styles-commons.ts",
     "pre:styles-components": "vite build --config vite.vendor-styles-components.ts",
-    "migrate": "node -r dotenv-flow/config node_modules/migrate-mongo/bin/migrate-mongo up -f config/migrate-mongo-config.js",
+    "migrate": "pnpm run migrate:migrate-mongo && pnpm run migrate:umzug",
+    "migrate:migrate-mongo": "node -r dotenv-flow/config node_modules/migrate-mongo/bin/migrate-mongo up -f config/migrate-mongo-config.js",
+    "migrate:umzug": "pnpm run ts-node prisma/migrate.ts up",
     "//// for development": "",
     "dev": "cross-env NODE_ENV=development nodemon --exec pnpm run ts-node --inspect src/server/app.ts",
     "dev:pre:styles-commons": "pnpm run pre:styles-commons --mode dev",
     "dev:pre:styles-components": "pnpm run pre:styles-components",
     "dev:migrate-mongo": "cross-env NODE_ENV=development pnpm run ts-node node_modules/migrate-mongo/bin/migrate-mongo",
+    "dev:umzug": "cross-env NODE_ENV=development pnpm run ts-node prisma/migrate.ts",
     "dev:migrate": "pnpm run dev:migrate:status > tmp/cache/migration-status.out && pnpm run dev:migrate:up",
-    "dev:migrate:status": "pnpm run dev:migrate-mongo status -f config/migrate-mongo-config.js",
-    "dev:migrate:up": "pnpm run dev:migrate-mongo up -f config/migrate-mongo-config.js",
-    "dev:migrate:down": "pnpm run dev:migrate-mongo down -f config/migrate-mongo-config.js",
+    "dev:migrate:status": "pnpm run dev:migrate-mongo status -f config/migrate-mongo-config.js && pnpm run dev:umzug executed && pnpm run dev:umzug pending",
+    "dev:migrate:up": "pnpm run dev:migrate-mongo up -f config/migrate-mongo-config.js && pnpm run dev:umzug up",
+    "dev:migrate:down": "pnpm run dev:umzug down && pnpm run dev:migrate-mongo down -f config/migrate-mongo-config.js",
     "//// for CI": "",
     "launch-dev:ci": "cross-env NODE_ENV=development pnpm run dev:migrate && pnpm run ts-node src/server/app.ts --ci",
     "lint:typecheck": "tsgo --noEmit",
@@ -45,6 +49,8 @@
     "//// misc": "",
     "console": "npm run repl",
     "repl": "cross-env NODE_ENV=development npm run ts-node src/server/repl.ts",
+    "prisma:generate": "prisma generate",
+    "prisma:pull": "prisma db pull",
     "openapi:build:generate-operation-ids": "vite build -c bin/openapi/generate-operation-ids/vite.config.ts",
     "openapi:generate-spec:apiv3": "sh bin/openapi/generate-spec-apiv3.sh",
     "openapi:generate-spec:apiv1": "sh bin/openapi/generate-spec-apiv1.sh",
@@ -109,11 +115,13 @@
     "@opentelemetry/sdk-node": "^0.202.0",
     "@opentelemetry/sdk-trace-node": "^2.0.1",
     "@opentelemetry/semantic-conventions": "^1.34.0",
+    "@prisma/client": "^6.19.2",
     "@replit/codemirror-emacs": "^6.1.0",
     "@replit/codemirror-vim": "^6.2.1",
     "@replit/codemirror-vscode-keymap": "^6.0.2",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
+    "@swc/helpers": "^0.5.18",
     "@tanstack/react-virtual": "^3.13.12",
     "@types/async": "^3.2.24",
     "@types/multer": "^1.4.12",
@@ -267,9 +275,12 @@
     "swr": "^2.3.2",
     "throttle-debounce": "^5.0.0",
     "ts-deepmerge": "^6.2.0",
+    "ts-node": "^10.9.2",
+    "tsconfig-paths": "^4.2.0",
     "tslib": "^2.8.0",
     "uglifycss": "^0.0.29",
     "uid-safe": "^2.1.5",
+    "umzug": "^3.8.2",
     "unified": "^11.0.0",
     "unist-util-visit": "^5.0.0",
     "unstated": "^2.1.1",
@@ -334,6 +345,7 @@
     "mongodb-connection-string-url": "^7.0.0",
     "mongodb-memory-server-core": "^9.1.1",
     "openapi-typescript": "^7.8.0",
+    "prisma": "^6.19.2",
     "rehype-rewrite": "^4.0.2",
     "remark-github-admonitions-to-directives": "^2.0.0",
     "sass": "^1.53.0",

+ 16 - 0
apps/app/prisma.config.ts

@@ -0,0 +1,16 @@
+import { config } from 'dotenv-flow';
+import { defineConfig } from 'prisma/config';
+
+config();
+
+// biome-ignore lint/style/noDefaultExport: prisma requires a default export
+export default defineConfig({
+  schema: 'prisma/schema.prisma',
+  migrations: {
+    path: 'prisma/migrations',
+  },
+  engine: 'classic',
+  datasource: {
+    url: process.env.MONGO_URI,
+  },
+});

+ 37 - 0
apps/app/prisma/migrate.ts

@@ -0,0 +1,37 @@
+/**
+ * umzug cli
+ *
+ * Usage:
+ *   pnpm ts-node prisma/migrate.ts
+ */
+import { resolve } from 'node:path';
+import { MongoClient } from 'mongodb';
+import { MongoDBStorage, Umzug } from 'umzug';
+
+(async () => {
+  const url = process.env.MONGO_URI;
+  if (url === undefined) {
+    throw new Error('MONGO_URI is required');
+  }
+  const { prisma } = await import(
+    process.env.NODE_ENV === 'production'
+      ? '../dist/utils/prisma'
+      : '../src/utils/prisma'
+  );
+  const client = new MongoClient(url);
+  await client.connect();
+
+  const umzug = new Umzug({
+    migrations: { glob: resolve(__dirname, '../prisma/migrations/*.(ts|js)') },
+    context: prisma,
+    storage: new MongoDBStorage({
+      connection: client.db(),
+    }),
+    logger: console,
+  });
+
+  if (require.main === module) {
+    await umzug.runAsCLI();
+    process.exit(0);
+  }
+})();

+ 0 - 0
apps/app/prisma/migrations/.keep


+ 516 - 0
apps/app/prisma/schema.prisma

@@ -0,0 +1,516 @@
+generator client {
+  provider = "prisma-client"
+  output   = "../src/generated/prisma"
+}
+
+datasource db {
+  provider = "mongodb"
+  url      = env("MONGO_URI")
+}
+
+type ActivitiesSnapshot {
+  id       String @map("_id") @db.ObjectId
+  username String
+}
+
+type AiassistantsGrantedGroupsForAccessScope {
+  /// Field referred in an index, but found no data to define the type.
+  item Json?
+}
+
+type AiassistantsGrantedGroupsForShareScope {
+  /// Field referred in an index, but found no data to define the type.
+  item Json?
+}
+
+type PageoperationsExPage {
+  /// Field referred in an index, but found no data to define the type.
+  id   Json? @map("_id")
+  /// Field referred in an index, but found no data to define the type.
+  path Json?
+}
+
+type PageoperationsPage {
+  /// Field referred in an index, but found no data to define the type.
+  id   Json? @map("_id")
+  /// Field referred in an index, but found no data to define the type.
+  path Json?
+}
+
+type PagesGrantedGroups {
+  /// Field referred in an index, but found no data to define the type.
+  item Json?
+}
+
+model accesstokens {
+  id        String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  expiredAt Json?
+  /// Field referred in an index, but found no data to define the type.
+  tokenHash Json?  @unique(map: "tokenHash_1")
+
+  @@index([expiredAt], map: "expiredAt_1")
+}
+
+model activities {
+  id          String             @id @default(auto()) @map("_id") @db.ObjectId
+  v           Int                @map("__v")
+  action      String
+  createdAt   DateTime           @db.Date
+  endpoint    String
+  ip          String
+  snapshot    ActivitiesSnapshot
+  target      String?            @db.ObjectId
+  targetModel String?
+  user        String?            @db.ObjectId
+
+  @@unique([user, target, action, createdAt], map: "user_1_target_1_action_1_createdAt_1")
+  @@index([user], map: "user_1")
+  @@index([snapshot.username], map: "snapshot.username_1")
+  @@index([target, action], map: "target_1_action_1")
+  @@index([createdAt], map: "createdAt_1")
+}
+
+model aiassistants {
+  id                          String                                   @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  grantedGroupsForAccessScope AiassistantsGrantedGroupsForAccessScope?
+  /// Field referred in an index, but found no data to define the type.
+  grantedGroupsForShareScope  AiassistantsGrantedGroupsForShareScope?
+
+  @@index([grantedGroupsForShareScope.item], map: "grantedGroupsForShareScope.item_1")
+  @@index([grantedGroupsForAccessScope.item], map: "grantedGroupsForAccessScope.item_1")
+}
+
+model attachments {
+  id       String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  creator  Json?
+  /// Field referred in an index, but found no data to define the type.
+  fileName Json?  @unique(map: "fileName_1")
+  /// Field referred in an index, but found no data to define the type.
+  page     Json?
+
+  @@index([page], map: "page_1")
+  @@index([creator], map: "creator_1")
+}
+
+model bookmarkfolders {
+  id    String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  owner Json?
+
+  @@index([owner], map: "owner_1")
+}
+
+model bookmarks {
+  id   String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  page Json?
+  /// Field referred in an index, but found no data to define the type.
+  user Json?
+
+  @@unique([page, user], map: "page_1_user_1")
+  @@index([page], map: "page_1")
+  @@index([user], map: "user_1")
+}
+
+model comments {
+  id       String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  creator  Json?
+  /// Field referred in an index, but found no data to define the type.
+  page     Json?
+  /// Field referred in an index, but found no data to define the type.
+  revision Json?
+
+  @@index([page], map: "page_1")
+  @@index([creator], map: "creator_1")
+  @@index([revision], map: "revision_1")
+}
+
+model configs {
+  id        String   @id @default(auto()) @map("_id") @db.ObjectId
+  v         Int?     @map("__v")
+  createdAt DateTime @db.Date
+  key       String   @unique(map: "key_1")
+  updatedAt DateTime @db.Date
+  value     String
+}
+
+model editorsettings {
+  id String @id @default(auto()) @map("_id") @db.ObjectId
+}
+
+model externalaccounts {
+  id           String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  accountId    Json?
+  /// Field referred in an index, but found no data to define the type.
+  providerType Json?
+
+  @@unique([providerType, accountId], map: "providerType_1_accountId_1")
+}
+
+model externalusergrouprelations {
+  id String @id @default(auto()) @map("_id") @db.ObjectId
+}
+
+model externalusergroups {
+  id         String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  externalId Json?  @unique(map: "externalId_1")
+  /// Field referred in an index, but found no data to define the type.
+  name       Json?
+  /// Field referred in an index, but found no data to define the type.
+  parent     Json?
+  /// Field referred in an index, but found no data to define the type.
+  provider   Json?
+
+  @@unique([name, provider], map: "name_1_provider_1")
+  @@index([parent], map: "parent_1")
+}
+
+model failedemails {
+  id        String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  createdAt Json?
+
+  @@index([createdAt], map: "createdAt_1")
+}
+
+model globalnotificationsettings {
+  id String @id @default(auto()) @map("_id") @db.ObjectId
+}
+
+model growiplugins {
+  id String @id @default(auto()) @map("_id") @db.ObjectId
+}
+
+model inappnotifications {
+  id        String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  action    Json?
+  /// Field referred in an index, but found no data to define the type.
+  createdAt Json?
+  /// Field referred in an index, but found no data to define the type.
+  status    Json?
+  /// Field referred in an index, but found no data to define the type.
+  target    Json?
+  /// Field referred in an index, but found no data to define the type.
+  user      Json?
+
+  @@index([user], map: "user_1")
+  @@index([status], map: "status_1")
+  @@index([createdAt], map: "createdAt_1")
+  @@index([user, target, action, createdAt], map: "user_1_target_1_action_1_createdAt_1")
+}
+
+model inappnotificationsettings {
+  id String @id @default(auto()) @map("_id") @db.ObjectId
+}
+
+model migrations {
+  id        String   @id @default(auto()) @map("_id") @db.ObjectId
+  appliedAt DateTime @db.Date
+  fileName  String
+}
+
+model namedqueries {
+  id            String @id @default(auto()) @map("_id") @db.ObjectId
+  v             Int    @map("__v")
+  /// Could not determine type: the field only had null or empty values in the sample set.
+  creator       Json?
+  delegatorName String
+  name          String @unique(map: "name_1")
+
+  @@index([creator], map: "creator_1")
+}
+
+model pagebulkexportjobs {
+  id String @id @default(auto()) @map("_id") @db.ObjectId
+}
+
+model pagebulkexportpagesnapshots {
+  id String @id @default(auto()) @map("_id") @db.ObjectId
+}
+
+model pageoperations {
+  id          String                @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  actionStage Json?
+  /// Field referred in an index, but found no data to define the type.
+  actionType  Json?
+  /// Field referred in an index, but found no data to define the type.
+  exPage      PageoperationsExPage?
+  /// Field referred in an index, but found no data to define the type.
+  fromPath    Json?
+  /// Field referred in an index, but found no data to define the type.
+  page        PageoperationsPage?
+  /// Field referred in an index, but found no data to define the type.
+  toPath      Json?
+
+  @@index([actionType], map: "actionType_1")
+  @@index([actionStage], map: "actionStage_1")
+  @@index([fromPath], map: "fromPath_1")
+  @@index([toPath], map: "toPath_1")
+  @@index([page.id], map: "page._id_1")
+  @@index([page.path], map: "page.path_1")
+  @@index([exPage.id], map: "exPage._id_1")
+  @@index([exPage.path], map: "exPage.path_1")
+}
+
+model pageredirects {
+  id       String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  fromPath Json?  @unique(map: "fromPath_1")
+}
+
+model pages {
+  id                       String      @id @default(auto()) @map("_id") @db.ObjectId
+  v                        Int         @map("__v")
+  commentCount             Int
+  createdAt                DateTime    @db.Date
+  creator                  String?     @db.ObjectId
+  descendantCount          Int
+  grant                    Int
+  /// Nested objects had no data in the sample dataset to introspect a nested type.
+  grantedGroups            Json?
+  /// Could not determine type: the field only had null or empty values in the sample set.
+  grantedUsers             Json?
+  isEmpty                  Boolean
+  lastUpdateUser           String?     @db.ObjectId
+  latestRevisionBodyLength Int?
+  /// Could not determine type: the field only had null or empty values in the sample set.
+  liker                    Json?
+  parent                   String?     @db.ObjectId
+  path                     String
+  revision                 String?     @db.ObjectId
+  seenUsers                String[]
+  status                   String
+  ttlTimestamp             DateTime?   @db.Date
+  updatedAt                DateTime    @db.Date
+  wip                      Boolean?
+  revisions                revisions[]
+
+  @@index([parent], map: "parent_1")
+  @@index([path], map: "path_1")
+  @@index([status], map: "status_1")
+  @@index([grant], map: "grant_1")
+  @@index([creator], map: "creator_1")
+  @@index([createdAt], map: "createdAt_1")
+  @@index([updatedAt], map: "updatedAt_1")
+  @@index([ttlTimestamp], map: "ttlTimestamp_1")
+}
+
+model pagetagrelations {
+  id            String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  isPageTrashed Json?
+  /// Field referred in an index, but found no data to define the type.
+  relatedPage   Json?
+  /// Field referred in an index, but found no data to define the type.
+  relatedTag    Json?
+
+  @@unique([relatedPage, relatedTag], map: "relatedPage_1_relatedTag_1")
+  @@index([relatedPage], map: "relatedPage_1")
+  @@index([relatedTag], map: "relatedTag_1")
+  @@index([isPageTrashed], map: "isPageTrashed_1")
+}
+
+model passwordresetorders {
+  id    String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  token Json?  @unique(map: "token_1")
+}
+
+model revisions {
+  id        String   @id @default(auto()) @map("_id") @db.ObjectId
+  v         Int      @map("__v")
+  author    String   @db.ObjectId
+  body      String
+  createdAt DateTime @db.Date
+  format    String
+  origin    String?
+  pageId    String   @db.ObjectId
+
+  page pages @relation(fields: [pageId], references: [id])
+
+  @@index([pageId], map: "pageId_1")
+}
+
+model rlflx {
+  id     String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  expire Json?
+  /// Field referred in an index, but found no data to define the type.
+  key    Json?  @unique(map: "key_1")
+
+  @@index([expire(sort: Desc)], map: "expire_-1")
+}
+
+model sessions {
+  id      String   @id @map("_id")
+  expires DateTime @db.Date
+  session String
+
+  @@index([expires], map: "expires_1")
+}
+
+model sharelinks {
+  id          String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  relatedPage Json?
+
+  @@index([relatedPage], map: "relatedPage_1")
+}
+
+model slackappintegrations {
+  id        String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  isPrimary Json?  @unique(map: "isPrimary_1")
+  /// Field referred in an index, but found no data to define the type.
+  tokenGtoP Json?  @unique(map: "tokenGtoP_1")
+  /// Field referred in an index, but found no data to define the type.
+  tokenPtoG Json?  @unique(map: "tokenPtoG_1")
+}
+
+model subscriptions {
+  id   String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  user Json?
+
+  @@index([user], map: "user_1")
+}
+
+model tags {
+  id   String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  name Json?  @unique(map: "name_1")
+}
+
+model threadrelations {
+  id       String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  threadId Json?  @unique(map: "threadId_1")
+}
+
+model transferkeys {
+  id        String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  expireAt  Json?
+  /// Field referred in an index, but found no data to define the type.
+  key       Json?  @unique(map: "key_1")
+  /// Field referred in an index, but found no data to define the type.
+  keyString Json?  @unique(map: "keyString_1")
+
+  @@index([expireAt], map: "expireAt_1")
+}
+
+model updateposts {
+  id      String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  creator Json?
+
+  @@index([creator], map: "creator_1")
+}
+
+model usergrouprelations {
+  id           String   @id @default(auto()) @map("_id") @db.ObjectId
+  v            Int      @map("__v")
+  createdAt    DateTime @db.Date
+  relatedGroup String   @db.ObjectId
+  relatedUser  String   @db.ObjectId
+}
+
+model usergroups {
+  id          String   @id @default(auto()) @map("_id") @db.ObjectId
+  v           Int      @map("__v")
+  createdAt   DateTime @db.Date
+  description String
+  name        String   @unique(map: "name_1")
+  /// Could not determine type: the field only had null or empty values in the sample set.
+  parent      Json?
+  updatedAt   DateTime @db.Date
+
+  @@index([parent], map: "parent_1")
+}
+
+model userregistrationorders {
+  id    String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  token Json?  @unique(map: "token_1")
+}
+
+model users {
+  id                      String   @id @default(auto()) @map("_id") @db.ObjectId
+  v                       Int      @map("__v")
+  admin                   Boolean
+  /// Field referred in an index, but found no data to define the type.
+  apiToken                Json?
+  createdAt               DateTime @db.Date
+  email                   String   @unique(map: "email_1")
+  imageUrlCached          String
+  isEmailPublished        Boolean
+  isGravatarEnabled       Boolean
+  isInvitationEmailSended Boolean
+  lang                    String
+  /// Field referred in an index, but found no data to define the type.
+  lastLoginAt             Json?
+  name                    String
+  password                String
+  readOnly                Boolean
+  /// Field referred in an index, but found no data to define the type.
+  slackMemberId           Json?    @unique(map: "slackMemberId_1")
+  status                  Int
+  updatedAt               DateTime @db.Date
+  username                String   @unique(map: "username_1")
+
+  @@index([name], map: "name_1")
+  @@index([apiToken], map: "apiToken_1")
+  @@index([status], map: "status_1")
+  @@index([lastLoginAt], map: "lastLoginAt_1")
+  @@index([admin], map: "admin_1")
+}
+
+model useruisettings {
+  id   String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  user Json?  @unique(map: "user_1")
+}
+
+model vectorstorefilerelations {
+  id                    String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  attachment            Json?
+  /// Field referred in an index, but found no data to define the type.
+  page                  Json?
+  /// Field referred in an index, but found no data to define the type.
+  vectorStoreRelationId Json?
+
+  @@unique([vectorStoreRelationId, page, attachment], map: "vectorStoreRelationId_1_page_1_attachment_1")
+}
+
+model vectorstores {
+  id            String @id @default(auto()) @map("_id") @db.ObjectId
+  /// Field referred in an index, but found no data to define the type.
+  vectorStoreId Json?  @unique(map: "vectorStoreId_1")
+}
+
+model yjs_writings {
+  id      String  @id @default(auto()) @map("_id") @db.ObjectId
+  action  String?
+  clock   Int?
+  docName String
+  metaKey String?
+  /// Field referred in an index, but found no data to define the type.
+  part    Json?
+  /// Multiple data types found: Float: 33.3%, Binary: 66.7% out of 3 sampled entries
+  value   Json
+  version String
+
+  @@index([version, docName, action, clock, part], map: "version_1_docName_1_action_1_clock_1_part_1")
+  @@index([version, docName, metaKey], map: "version_1_docName_1_metaKey_1")
+  @@index([docName, clock], map: "docName_1_clock_1")
+  @@map("yjs-writings")
+}

+ 6 - 0
apps/app/prisma/types.ts

@@ -0,0 +1,6 @@
+import type { PrismaClient } from '~/generated/prisma/client';
+
+/**
+ * Migration function type
+ */
+export type Migration = (args: { context: PrismaClient }) => Promise<void>;

+ 4 - 6
apps/app/src/client/components/Admin/AdminHome/AdminHome.tsx

@@ -1,5 +1,5 @@
 import type { FC } from 'react';
-import { useId, useState } from 'react';
+import { useRef, useState } from 'react';
 import { useTranslation } from 'next-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
 import { Tooltip } from 'reactstrap';
@@ -29,9 +29,7 @@ export const AdminHome: FC = () => {
     }, 500);
   };
 
-  // Generate CSS-safe ID by removing colons from useId() result
-  const copyButtonIdRaw = useId();
-  const copyButtonId = `copy-button-${copyButtonIdRaw.replace(/:/g, '')}`;
+  const copyIconRef = useRef<HTMLSpanElement>(null);
 
   return (
     <div data-testid="admin-home">
@@ -131,7 +129,7 @@ export const AdminHome: FC = () => {
                   onClick={(e) => e.preventDefault()}
                 >
                   <span
-                    id={copyButtonId}
+                    ref={copyIconRef}
                     className="material-symbols-outlined"
                     aria-hidden="true"
                   >
@@ -143,7 +141,7 @@ export const AdminHome: FC = () => {
               <Tooltip
                 placement="bottom"
                 isOpen={copyState === COPY_STATE.DONE}
-                target={copyButtonId}
+                target={copyIconRef}
                 fade={false}
               >
                 {t('admin:admin_top:copy_prefilled_host_information:done')}

+ 4 - 4
apps/app/src/client/components/PageEditor/EditorNavbar/EditingUserList.tsx

@@ -1,4 +1,4 @@
-import { type FC, useId, useState } from 'react';
+import { type FC, useRef, useState } from 'react';
 import type { EditingClient } from '@growi/editor';
 import { UserPicture } from '@growi/ui/dist/components';
 import { Popover, PopoverBody } from 'reactstrap';
@@ -31,7 +31,7 @@ const AvatarWrapper: FC<{
 };
 
 export const EditingUserList: FC<Props> = ({ clientList, onUserClick }) => {
-  const popoverTargetId = useId();
+  const popoverTargetRef = useRef<HTMLButtonElement>(null);
   const [isPopoverOpen, setIsPopoverOpen] = useState(false);
 
   const togglePopover = () => setIsPopoverOpen(!isPopoverOpen);
@@ -56,7 +56,7 @@ export const EditingUserList: FC<Props> = ({ clientList, onUserClick }) => {
           <div className="ms-1">
             <button
               type="button"
-              id={popoverTargetId}
+              ref={popoverTargetRef}
               className="btn border-0 bg-info-subtle rounded-pill p-0"
               onClick={togglePopover}
             >
@@ -67,7 +67,7 @@ export const EditingUserList: FC<Props> = ({ clientList, onUserClick }) => {
             <Popover
               placement="bottom"
               isOpen={isPopoverOpen}
-              target={popoverTargetId}
+              target={popoverTargetRef}
               toggle={togglePopover}
               trigger="legacy"
             >

+ 8 - 4
apps/app/src/features/page-tree/components/SimpleItemContent.tsx

@@ -1,4 +1,4 @@
-import { useId } from 'react';
+import { useRef } from 'react';
 import Link from 'next/link';
 import { pathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
@@ -26,7 +26,7 @@ export const SimpleItemContent = ({
   const shouldShowAttentionIcon =
     page.processData != null ? shouldRecoverPagePaths(page.processData) : false;
 
-  const spanId = `path-recovery-${useId()}`;
+  const warningIconRef = useRef<HTMLSpanElement>(null);
 
   // When asLink is true, render the title as an anchor so that the browser
   // recognizes it as a link (enables Ctrl/Cmd+click to open in new tab,
@@ -48,12 +48,16 @@ export const SimpleItemContent = ({
       {shouldShowAttentionIcon && (
         <>
           <span
-            id={spanId}
+            ref={warningIconRef}
             className="material-symbols-outlined mr-2 text-warning"
           >
             warning
           </span>
-          <UncontrolledTooltip placement="top" target={spanId} fade={false}>
+          <UncontrolledTooltip
+            placement="top"
+            target={warningIconRef}
+            fade={false}
+          >
             {t('tooltip.operation.attention.rename')}
           </UncontrolledTooltip>
         </>

+ 1 - 0
apps/app/src/features/page/index.ts

@@ -0,0 +1 @@
+export * from './models';

+ 1 - 0
apps/app/src/features/page/models/index.ts

@@ -0,0 +1 @@
+export * from './revision';

+ 5 - 0
apps/app/src/features/page/models/revision.ts

@@ -0,0 +1,5 @@
+import { Prisma } from '~/generated/prisma/client';
+
+export const extension = Prisma.defineExtension((client) =>
+  client.$extends({}),
+);

+ 5 - 0
apps/app/src/utils/prisma.ts

@@ -0,0 +1,5 @@
+import { extension as RevisionExtension } from '~/features/page';
+import { PrismaClient as OriginalPrismaClient } from '~/generated/prisma/client';
+
+export const prisma = new OriginalPrismaClient().$extends(RevisionExtension);
+export type PrismaClient = typeof prisma;

+ 1 - 1
apps/app/tsconfig.json

@@ -26,7 +26,7 @@
       { "transform": "typescript-transform-paths", "afterDeclarations": true }
     ]
   },
-  "include": ["next-env.d.ts", "config", "src"],
+  "include": ["next-env.d.ts", "config", "prisma", "src"],
   "exclude": ["src/**/*.vendor-styles.*"],
   "ts-node": {
     "transpileOnly": true,

+ 1 - 1
apps/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@growi/slackbot-proxy",
-  "version": "7.5.2-slackbot-proxy.0",
+  "version": "7.5.3-slackbot-proxy.0",
   "license": "MIT",
   "private": true,
   "scripts": {

+ 1 - 0
biome.json

@@ -17,6 +17,7 @@
       "!.vscode",
       "!.claude",
       "!tsconfig.base.json",
+      "!apps/app/src/generated",
       "!packages/pdf-converter-client/src/index.ts",
       "!packages/pdf-converter-client/specs"
     ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "7.5.2-RC.0",
+  "version": "7.5.3-RC.0",
   "description": "Team collaboration software using markdown",
   "license": "MIT",
   "private": true,

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 347 - 38
pnpm-lock.yaml


Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů