Merge branch 'develop' into fix-signout
This commit is contained in:
commit
208681de4b
|
|
@ -6,6 +6,7 @@
|
|||
Dockerfile
|
||||
build/
|
||||
built/
|
||||
src-js/
|
||||
db/
|
||||
.devcontainer/compose.yml
|
||||
node_modules/
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ docker-compose.yml
|
|||
built
|
||||
built-test
|
||||
js-built
|
||||
src-js
|
||||
/data
|
||||
/.cache-loader
|
||||
/db
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
- Enhance: ウィジェットの表示設定をプレビューを見ながら行えるように
|
||||
- Enhance: ウィジェットの設定項目のラベルの多言語対応
|
||||
- Enhance: アカウント管理ページで、全てのアカウントから一括でログアウトできるように
|
||||
- Enhance: パフォーマンスの向上
|
||||
- Fix: ドライブクリーナーでファイルを削除しても画面に反映されない問題を修正 #16061
|
||||
- Fix: 非ログイン時にログインを求めるダイアログが表示された後にダイアログのぼかしが解除されず操作不能になることがある問題を修正
|
||||
- Fix: ドライブのソートが「登録日(昇順)」の場合に正しく動作しない問題を修正
|
||||
|
|
@ -26,7 +27,7 @@
|
|||
- Enhance: OAuthのクライアント情報取得(Client Information Discovery)において、IndieWeb Living Standard 11 July 2024で定義されているJSONドキュメント形式に対応しました
|
||||
- JSONによるClient Information Discoveryを行うには、レスポンスの`Content-Type`ヘッダーが`application/json`である必要があります
|
||||
- 従来の実装(12 February 2022版・HTML Microformat形式)も引き続きサポートされます
|
||||
|
||||
- Enhance: メモリ使用量を削減
|
||||
|
||||
## 2025.12.2
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,7 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-js/
|
|||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-reversi/built ./packages/misskey-reversi/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/misskey-bubble-game/built ./packages/misskey-bubble-game/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/built ./packages/backend/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/src-js ./packages/backend/src-js
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/packages/i18n/built ./packages/i18n/built
|
||||
COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
|
||||
COPY --chown=misskey:misskey . ./
|
||||
|
|
|
|||
10
README.md
10
README.md
|
|
@ -51,3 +51,13 @@ Thanks to [Crowdin](https://crowdin.com/) for providing the localization platfor
|
|||
<a href="https://hub.docker.com/"><img src="https://user-images.githubusercontent.com/20679825/230148221-f8e73a32-a49b-47c3-9029-9a15c3824f92.png" height="30" alt="Docker" /></a>
|
||||
|
||||
Thanks to [Docker](https://hub.docker.com/) for providing the container platform that helps us run Misskey in production.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
Support us with a ⭐ !
|
||||
|
||||
[](https://star-history.com/#misskey-dev/misskey&Date)
|
||||
|
||||
</div>
|
||||
|
|
|
|||
30
package.json
30
package.json
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2026.1.0-alpha.0",
|
||||
"version": "2026.1.0-alpha.2",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@10.25.0",
|
||||
"packageManager": "pnpm@10.26.2",
|
||||
"workspaces": [
|
||||
"packages/misskey-js",
|
||||
"packages/i18n",
|
||||
|
|
@ -23,12 +23,12 @@
|
|||
"private": true,
|
||||
"scripts": {
|
||||
"compile-config": "cd packages/backend && pnpm compile-config",
|
||||
"build-pre": "node ./scripts/build-pre.js",
|
||||
"build-pre": "node scripts/build-pre.mjs",
|
||||
"build-assets": "node ./scripts/build-assets.mjs",
|
||||
"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
|
||||
"build-storybook": "pnpm --filter frontend build-storybook",
|
||||
"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
|
||||
"start": "pnpm check:connect && cd packages/backend && pnpm compile-config && node ./built/boot/entry.js",
|
||||
"start": "cd packages/backend && pnpm compile-config && node ./built/boot/entry.js",
|
||||
"start:inspect": "cd packages/backend && pnpm compile-config && node --inspect ./built/boot/entry.js",
|
||||
"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"cli": "cd packages/backend && pnpm cli",
|
||||
|
|
@ -48,8 +48,8 @@
|
|||
"jest-and-coverage": "cd packages/backend && pnpm jest-and-coverage",
|
||||
"test": "pnpm -r test",
|
||||
"test-and-coverage": "pnpm -r test-and-coverage",
|
||||
"clean": "node ./scripts/clean.js",
|
||||
"clean-all": "node ./scripts/clean-all.js",
|
||||
"clean": "node scripts/clean.mjs",
|
||||
"clean-all": "node scripts/clean-all.mjs",
|
||||
"cleanall": "pnpm clean-all"
|
||||
},
|
||||
"resolutions": {
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"cssnano": "7.1.2",
|
||||
"esbuild": "0.27.1",
|
||||
"esbuild": "0.27.2",
|
||||
"execa": "9.6.1",
|
||||
"ignore-walk": "8.0.0",
|
||||
"js-yaml": "4.1.1",
|
||||
|
|
@ -67,19 +67,19 @@
|
|||
"terser": "5.44.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.39.1",
|
||||
"@eslint/js": "9.39.2",
|
||||
"@misskey-dev/eslint-plugin": "2.2.0",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251206.1",
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@typescript/native-preview": "7.0.0-dev.20251226.1",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.7.1",
|
||||
"eslint": "9.39.1",
|
||||
"cypress": "15.8.1",
|
||||
"eslint": "9.39.2",
|
||||
"globals": "16.5.0",
|
||||
"ncp": "2.0.0",
|
||||
"pnpm": "10.25.0",
|
||||
"pnpm": "10.26.2",
|
||||
"typescript": "5.9.3",
|
||||
"start-server-and-test": "2.1.3"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
import fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { build } from 'esbuild';
|
||||
import { swcPlugin } from 'esbuild-plugin-swc';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
|
||||
|
||||
const resolveTsPathsPlugin = {
|
||||
name: 'resolve-ts-paths',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^\.{1,2}\/.*\.js$/ }, (args) => {
|
||||
if (args.importer) {
|
||||
const absPath = join(args.resolveDir, args.path);
|
||||
const tsPath = absPath.slice(0, -3) + '.ts';
|
||||
if (fs.existsSync(tsPath)) return { path: tsPath };
|
||||
const tsxPath = absPath.slice(0, -3) + '.tsx';
|
||||
if (fs.existsSync(tsxPath)) return { path: tsxPath };
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const externalIpaddrPlugin = {
|
||||
name: 'external-ipaddr',
|
||||
setup(build) {
|
||||
build.onResolve({ filter: /^ipaddr\.js$/ }, (args) => {
|
||||
return { path: args.path, external: true };
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('esbuild').BuildOptions} */
|
||||
const options = {
|
||||
entryPoints: ['./src/boot/entry.ts'],
|
||||
minify: true,
|
||||
keepNames: true,
|
||||
bundle: true,
|
||||
outdir: './built/boot',
|
||||
target: 'node22',
|
||||
platform: 'node',
|
||||
format: 'esm',
|
||||
sourcemap: 'linked',
|
||||
packages: 'external',
|
||||
banner: {
|
||||
js: 'import { createRequire as topLevelCreateRequire } from "module";' +
|
||||
'import ___url___ from "url";' +
|
||||
'const require = topLevelCreateRequire(import.meta.url);' +
|
||||
'const __filename = ___url___.fileURLToPath(import.meta.url);' +
|
||||
'const __dirname = ___url___.fileURLToPath(new URL(".", import.meta.url));',
|
||||
},
|
||||
plugins: [
|
||||
externalIpaddrPlugin,
|
||||
resolveTsPathsPlugin,
|
||||
swcPlugin({
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
decorators: true,
|
||||
dynamicImport: true,
|
||||
},
|
||||
transform: {
|
||||
legacyDecorator: true,
|
||||
decoratorMetadata: true,
|
||||
},
|
||||
experimental: {
|
||||
keepImportAssertions: true,
|
||||
},
|
||||
baseUrl: join(_dirname, 'src'),
|
||||
paths: {
|
||||
'@/*': ['*'],
|
||||
},
|
||||
target: 'esnext',
|
||||
keepClassNames: true,
|
||||
},
|
||||
}),
|
||||
externalIpaddrPlugin,
|
||||
],
|
||||
// external: [
|
||||
// 'slacc-*',
|
||||
// 'class-transformer',
|
||||
// 'class-validator',
|
||||
// '@sentry/*',
|
||||
// '@nestjs/websockets/socket-module',
|
||||
// '@nestjs/microservices/microservices-module',
|
||||
// '@nestjs/microservices',
|
||||
// '@napi-rs/canvas-win32-x64-msvc',
|
||||
// 'mock-aws-s3',
|
||||
// 'aws-sdk',
|
||||
// 'nock',
|
||||
// 'sharp',
|
||||
// 'jsdom',
|
||||
// 're2',
|
||||
// '@napi-rs/canvas',
|
||||
// ],
|
||||
};
|
||||
|
||||
const args = process.argv.slice(2).map(arg => arg.toLowerCase());
|
||||
|
||||
if (!args.includes('--no-clean')) {
|
||||
fs.rmSync('./built', { recursive: true, force: true });
|
||||
}
|
||||
|
||||
await buildSrc();
|
||||
|
||||
async function buildSrc() {
|
||||
console.log(`[${_package.name}] start building...`);
|
||||
|
||||
await build(options)
|
||||
.then(() => {
|
||||
console.log(`[${_package.name}] build succeeded.`);
|
||||
})
|
||||
.catch((err) => {
|
||||
process.stderr.write(err.stderr || err.message || err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
console.log(`[${_package.name}] finish building.`);
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { DataSource } from 'typeorm';
|
||||
import { loadConfig } from './built/config.js';
|
||||
import { entities } from './built/postgres.js';
|
||||
import { loadConfig } from './src-js/config.js';
|
||||
import { entities } from './src-js/postgres.js';
|
||||
|
||||
const isConcurrentIndexMigrationEnabled = process.env.MISSKEY_MIGRATION_CREATE_INDEX_CONCURRENTLY === '1';
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@
|
|||
"start:test": "cross-env NODE_ENV=test pnpm compile-config && cross-env NODE_ENV=test node ./built/boot/entry.js",
|
||||
"migrate": "pnpm compile-config && pnpm typeorm migration:run -d ormconfig.js",
|
||||
"revert": "pnpm compile-config && pnpm typeorm migration:revert -d ormconfig.js",
|
||||
"cli": "pnpm compile-config && node ./built/boot/cli.js",
|
||||
"cli": "pnpm compile-config && node ./src-js/boot/cli.js",
|
||||
"check:connect": "pnpm compile-config && node ./scripts/check_connect.js",
|
||||
"compile-config": "node ./scripts/compile_config.js",
|
||||
"build": "swc src -d built -D --strip-leading-paths",
|
||||
"build": "swc src -d src-js -D --strip-leading-paths && node ./build.js",
|
||||
"build:test": "swc test-server -d built-test -D --config-file test-server/.swcrc --strip-leading-paths",
|
||||
"watch:swc": "swc src -d built -D -w --strip-leading-paths",
|
||||
"build:tsc": "tsgo -p tsconfig.json && tsc-alias -p tsconfig.json",
|
||||
|
|
@ -219,6 +219,7 @@
|
|||
"aws-sdk-client-mock": "4.1.0",
|
||||
"cbor": "10.0.11",
|
||||
"cross-env": "10.1.0",
|
||||
"esbuild-plugin-swc": "1.0.1",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"execa": "9.6.1",
|
||||
"fkill": "10.0.1",
|
||||
|
|
|
|||
|
|
@ -4,8 +4,8 @@
|
|||
*/
|
||||
|
||||
import Redis from 'ioredis';
|
||||
import { loadConfig } from '../built/config.js';
|
||||
import { createPostgresDataSource } from '../built/postgres.js';
|
||||
import { loadConfig } from '../src-js/config.js';
|
||||
import { createPostgresDataSource } from '../src-js/postgres.js';
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
|
|
@ -28,10 +28,8 @@ async function connectToRedis(redisOptions) {
|
|||
try {
|
||||
await redis.connect();
|
||||
resolve();
|
||||
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
|
||||
} finally {
|
||||
redis.disconnect(false);
|
||||
}
|
||||
|
|
@ -50,7 +48,7 @@ const promises = Array
|
|||
]))
|
||||
.map(connectToRedis)
|
||||
.concat([
|
||||
connectToPostgres()
|
||||
connectToPostgres(),
|
||||
]);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { writeFileSync, existsSync } from 'node:fs';
|
||||
import { execa } from 'execa';
|
||||
import { writeFileSync, existsSync } from "node:fs";
|
||||
|
||||
async function main() {
|
||||
if (!process.argv.includes('--no-build')) {
|
||||
|
|
@ -19,10 +19,10 @@ async function main() {
|
|||
}
|
||||
|
||||
/** @type {import('../src/config.js')} */
|
||||
const { loadConfig } = await import('../built/config.js');
|
||||
const { loadConfig } = await import('../src-js/config.js');
|
||||
|
||||
/** @type {import('../src/server/api/openapi/gen-spec.js')} */
|
||||
const { genOpenapiSpec } = await import('../built/server/api/openapi/gen-spec.js');
|
||||
const { genOpenapiSpec } = await import('../src-js/server/api/openapi/gen-spec.js');
|
||||
|
||||
const config = loadConfig();
|
||||
const spec = genOpenapiSpec(config, true);
|
||||
|
|
|
|||
|
|
@ -4,8 +4,6 @@
|
|||
*/
|
||||
|
||||
import * as fs from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname } from 'node:path';
|
||||
import * as os from 'node:os';
|
||||
import cluster from 'node:cluster';
|
||||
import chalk from 'chalk';
|
||||
|
|
@ -17,20 +15,15 @@ import { showMachineInfo } from '@/misc/show-machine-info.js';
|
|||
import { envOption } from '@/env.js';
|
||||
import { jobQueue, server } from './common.js';
|
||||
|
||||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../../built/meta.json`, 'utf-8'));
|
||||
|
||||
const logger = new Logger('core', 'cyan');
|
||||
const bootLogger = logger.createSubLogger('boot', 'magenta');
|
||||
|
||||
const themeColor = chalk.hex('#86b300');
|
||||
|
||||
function greet() {
|
||||
function greet(props: { version: string }) {
|
||||
if (!envOption.quiet) {
|
||||
//#region Misskey logo
|
||||
const v = `v${meta.version}`;
|
||||
const v = `v${props.version}`;
|
||||
console.log(themeColor(' _____ _ _ '));
|
||||
console.log(themeColor(' | |_|___ ___| |_ ___ _ _ '));
|
||||
console.log(themeColor(' | | | | |_ -|_ -| \'_| -_| | |'));
|
||||
|
|
@ -46,7 +39,7 @@ function greet() {
|
|||
}
|
||||
|
||||
bootLogger.info('Welcome to Misskey!');
|
||||
bootLogger.info(`Misskey v${meta.version}`, null, true);
|
||||
bootLogger.info(`Misskey v${props.version}`, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -57,11 +50,11 @@ export async function masterMain() {
|
|||
|
||||
// initialize app
|
||||
try {
|
||||
greet();
|
||||
config = loadConfigBoot();
|
||||
greet({ version: config.version });
|
||||
showEnvironment();
|
||||
await showMachineInfo(bootLogger);
|
||||
showNodejsVersion();
|
||||
config = loadConfigBoot();
|
||||
//await connectDb();
|
||||
if (config.pidFile) fs.writeFileSync(config.pidFile, process.pid.toString());
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -219,24 +219,42 @@ export type FulltextSearchProvider = 'sqlLike' | 'sqlPgroonga' | 'meilisearch';
|
|||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const compiledConfigFilePathForTest = resolve(_dirname, '../../../built/._config_.json');
|
||||
/** Path of repository root directory */
|
||||
let rootDir = _dirname;
|
||||
// 見つかるまで上に遡る
|
||||
while (!fs.existsSync(resolve(rootDir, 'packages'))) {
|
||||
const parentDir = dirname(rootDir);
|
||||
if (parentDir === rootDir) {
|
||||
throw new Error('Cannot find root directory');
|
||||
}
|
||||
rootDir = parentDir;
|
||||
}
|
||||
|
||||
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest) ? compiledConfigFilePathForTest : resolve(_dirname, '../../../built/.config.json');
|
||||
/** Path of configuration directory */
|
||||
const configDir = resolve(rootDir, '.config');
|
||||
/** Path of built directory */
|
||||
const projectBuiltDir = resolve(rootDir, 'built');
|
||||
|
||||
const compiledConfigFilePathForTest = resolve(projectBuiltDir, '._config_.json');
|
||||
|
||||
export const compiledConfigFilePath = fs.existsSync(compiledConfigFilePathForTest)
|
||||
? compiledConfigFilePathForTest
|
||||
: resolve(projectBuiltDir, '.config.json');
|
||||
|
||||
export function loadConfig(): Config {
|
||||
if (!fs.existsSync(compiledConfigFilePath)) {
|
||||
throw new Error('Compiled configuration file not found. Try running \'pnpm compile-config\'.');
|
||||
}
|
||||
|
||||
const meta = JSON.parse(fs.readFileSync(`${_dirname}/../../../built/meta.json`, 'utf-8'));
|
||||
const meta = JSON.parse(fs.readFileSync(resolve(projectBuiltDir, 'meta.json'), 'utf-8'));
|
||||
|
||||
const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
|
||||
const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
|
||||
const frontendManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'));
|
||||
const frontendEmbedManifestExists = fs.existsSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'));
|
||||
const frontendManifest = frontendManifestExists ?
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
|
||||
JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_vite_/manifest.json'), 'utf-8'))
|
||||
: { 'src/_boot_.ts': { file: null } };
|
||||
const frontendEmbedManifest = frontendEmbedManifestExists ?
|
||||
JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
|
||||
JSON.parse(fs.readFileSync(resolve(projectBuiltDir, '_frontend_embed_vite_/manifest.json'), 'utf-8'))
|
||||
: { 'src/boot.ts': { file: null } };
|
||||
|
||||
const config = JSON.parse(fs.readFileSync(compiledConfigFilePath, 'utf-8')) as Source;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { deepClone } from '@/misc/clone.js';
|
|||
import { bindThis } from '@/decorators.js';
|
||||
import { isMimeImage } from '@/misc/is-mime-image.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { uniqueByKey } from '@/misc/unique-by-key.js';
|
||||
import { UtilityService } from '../UtilityService.js';
|
||||
import { VideoProcessingService } from '../VideoProcessingService.js';
|
||||
import { UserEntityService } from './UserEntityService.js';
|
||||
|
|
@ -226,6 +227,7 @@ export class DriveFileEntityService {
|
|||
options?: PackOptions,
|
||||
hint?: {
|
||||
packedUser?: Packed<'UserLite'>
|
||||
packedFolder?: Packed<'DriveFolder'>
|
||||
},
|
||||
): Promise<Packed<'DriveFile'> | null> {
|
||||
const opts = Object.assign({
|
||||
|
|
@ -250,9 +252,9 @@ export class DriveFileEntityService {
|
|||
thumbnailUrl: this.getThumbnailUrl(file),
|
||||
comment: file.comment,
|
||||
folderId: file.folderId,
|
||||
folder: opts.detail && file.folderId ? this.driveFolderEntityService.pack(file.folderId, {
|
||||
folder: opts.detail && file.folderId ? (hint?.packedFolder ?? this.driveFolderEntityService.pack(file.folderId, {
|
||||
detail: true,
|
||||
}) : null,
|
||||
})) : null,
|
||||
userId: file.userId,
|
||||
user: (opts.withUser && file.userId) ? hint?.packedUser ?? this.userEntityService.pack(file.userId) : null,
|
||||
});
|
||||
|
|
@ -263,10 +265,41 @@ export class DriveFileEntityService {
|
|||
files: MiDriveFile[],
|
||||
options?: PackOptions,
|
||||
): Promise<Packed<'DriveFile'>[]> {
|
||||
const _user = files.map(({ user, userId }) => user ?? userId).filter(x => x != null);
|
||||
const _userMap = await this.userEntityService.packMany(_user)
|
||||
.then(users => new Map(users.map(user => [user.id, user])));
|
||||
const items = await Promise.all(files.map(f => this.packNullable(f, options, f.userId ? { packedUser: _userMap.get(f.userId) } : {})));
|
||||
// -- ユーザ情報の事前取得 --
|
||||
|
||||
let userMap: Map<string, Packed<'UserLite'>> | null = null;
|
||||
if (options?.withUser) {
|
||||
const users = files
|
||||
.map(({ user, userId }) => user ?? userId)
|
||||
.filter(x => x != null);
|
||||
|
||||
const uniqueUsers = uniqueByKey(users, (user) => typeof user === 'string' ? user : user.id);
|
||||
const packedUsers = await this.userEntityService.packMany(uniqueUsers);
|
||||
userMap = new Map(packedUsers.map(user => [user.id, user]));
|
||||
}
|
||||
|
||||
// -- フォルダ情報の事前取得 --
|
||||
|
||||
let folderMap: Map<string, Packed<'DriveFolder'>> | null = null;
|
||||
if (options?.detail) {
|
||||
const folders = files
|
||||
.map(({ folder, folderId }) => folder ?? folderId)
|
||||
.filter(x => x != null);
|
||||
|
||||
const uniqueFolders = uniqueByKey(folders, (folder) => typeof folder === 'string' ? folder : folder.id);
|
||||
const packedFolders = await this.driveFolderEntityService.packMany(uniqueFolders, { detail: true });
|
||||
folderMap = new Map(packedFolders.map(folder => [folder.id, folder]));
|
||||
}
|
||||
|
||||
const items = await Promise.all(files.map(f => this.packNullable(
|
||||
f,
|
||||
options,
|
||||
{
|
||||
packedUser: f.userId ? userMap?.get(f.userId) : undefined,
|
||||
packedFolder: f.folderId ? folderMap?.get(f.folderId) : undefined,
|
||||
},
|
||||
)));
|
||||
|
||||
return items.filter(x => x != null);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ import type { } from '@/models/Blocking.js';
|
|||
import type { MiDriveFolder } from '@/models/DriveFolder.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { IdService } from '@/core/IdService.js';
|
||||
import { In } from 'typeorm';
|
||||
import { uniqueByKey } from '@/misc/unique-by-key.js';
|
||||
import { splitIdAndObjects } from '@/misc/split-id-and-objects.js';
|
||||
|
||||
@Injectable()
|
||||
export class DriveFolderEntityService {
|
||||
|
|
@ -32,12 +35,20 @@ export class DriveFolderEntityService {
|
|||
options?: {
|
||||
detail: boolean
|
||||
},
|
||||
hint?: {
|
||||
folderMap?: Map<string, MiDriveFolder>;
|
||||
foldersCountMap?: Map<string, number> | null;
|
||||
filesCountMap?: Map<string, number> | null;
|
||||
parentPacker?: (id: string) => Promise<Packed<'DriveFolder'>>;
|
||||
},
|
||||
): Promise<Packed<'DriveFolder'>> {
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
}, options);
|
||||
|
||||
const folder = typeof src === 'object' ? src : await this.driveFoldersRepository.findOneByOrFail({ id: src });
|
||||
const folder = typeof src === 'object'
|
||||
? src
|
||||
: hint?.folderMap?.get(src) ?? await this.driveFoldersRepository.findOneByOrFail({ id: src });
|
||||
|
||||
return await awaitAll({
|
||||
id: folder.id,
|
||||
|
|
@ -46,20 +57,141 @@ export class DriveFolderEntityService {
|
|||
parentId: folder.parentId,
|
||||
|
||||
...(opts.detail ? {
|
||||
foldersCount: this.driveFoldersRepository.countBy({
|
||||
parentId: folder.id,
|
||||
}),
|
||||
filesCount: this.driveFilesRepository.countBy({
|
||||
folderId: folder.id,
|
||||
}),
|
||||
foldersCount: hint?.foldersCountMap?.get(folder.id)
|
||||
?? this.driveFoldersRepository.countBy({
|
||||
parentId: folder.id,
|
||||
}),
|
||||
filesCount: hint?.filesCountMap?.get(folder.id)
|
||||
?? this.driveFilesRepository.countBy({
|
||||
folderId: folder.id,
|
||||
}),
|
||||
|
||||
...(folder.parentId ? {
|
||||
parent: this.pack(folder.parentId, {
|
||||
detail: true,
|
||||
}),
|
||||
parent: hint?.parentPacker
|
||||
? hint.parentPacker(folder.parentId)
|
||||
: this.pack(folder.parentId, { detail: true }, hint),
|
||||
} : {}),
|
||||
} : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async packMany(
|
||||
src: Array<MiDriveFolder['id'] | MiDriveFolder>,
|
||||
options?: {
|
||||
detail: boolean
|
||||
},
|
||||
): Promise<Array<Packed<'DriveFolder'>>> {
|
||||
/**
|
||||
* 重複を除去しつつ、必要なDriveFolderオブジェクトをすべて取得する
|
||||
*/
|
||||
const collectUniqueObjects = async (src: Array<MiDriveFolder['id'] | MiDriveFolder>) => {
|
||||
const uniqueSrc = uniqueByKey(
|
||||
src,
|
||||
(s) => typeof s === 'string' ? s : s.id,
|
||||
);
|
||||
const { ids, objects } = splitIdAndObjects(uniqueSrc);
|
||||
|
||||
const uniqueObjects = new Map<string, MiDriveFolder>(objects.map(s => [s.id, s]));
|
||||
const needsFetchIds = ids.filter(id => !uniqueObjects.has(id));
|
||||
|
||||
if (needsFetchIds.length > 0) {
|
||||
const fetchedObjects = await this.driveFoldersRepository.find({
|
||||
where: {
|
||||
id: In(needsFetchIds),
|
||||
},
|
||||
});
|
||||
for (const obj of fetchedObjects) {
|
||||
uniqueObjects.set(obj.id, obj);
|
||||
}
|
||||
}
|
||||
|
||||
return uniqueObjects;
|
||||
};
|
||||
|
||||
/**
|
||||
* 親フォルダーを再帰的に収集する
|
||||
*/
|
||||
const collectAncestors = async (folderMap: Map<string, MiDriveFolder>) => {
|
||||
for (;;) {
|
||||
const parentIds = new Set<string>();
|
||||
for (const folder of folderMap.values()) {
|
||||
if (folder.parentId != null && !folderMap.has(folder.parentId)) {
|
||||
parentIds.add(folder.parentId);
|
||||
}
|
||||
}
|
||||
|
||||
if (parentIds.size === 0) break;
|
||||
|
||||
const fetchedParents = await this.driveFoldersRepository.find({
|
||||
where: {
|
||||
id: In([...parentIds]),
|
||||
},
|
||||
});
|
||||
|
||||
if (fetchedParents.length === 0) break;
|
||||
|
||||
for (const parent of fetchedParents) {
|
||||
folderMap.set(parent.id, parent);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const opts = Object.assign({
|
||||
detail: false,
|
||||
}, options);
|
||||
|
||||
const folderMap = await collectUniqueObjects(src);
|
||||
|
||||
let foldersCountMap: Map<string, number> | null = null;
|
||||
let filesCountMap: Map<string, number> | null = null;
|
||||
if (opts.detail) {
|
||||
await collectAncestors(folderMap);
|
||||
|
||||
const ids = [...folderMap.keys()];
|
||||
if (ids.length > 0) {
|
||||
const folderCounts = await this.driveFoldersRepository.createQueryBuilder('folder')
|
||||
.select('folder.parentId', 'parentId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('folder.parentId IN (:...ids)', { ids })
|
||||
.groupBy('folder.parentId')
|
||||
.getRawMany<{ parentId: string; count: string }>();
|
||||
|
||||
const fileCounts = await this.driveFilesRepository.createQueryBuilder('file')
|
||||
.select('file.folderId', 'folderId')
|
||||
.addSelect('COUNT(*)', 'count')
|
||||
.where('file.folderId IN (:...ids)', { ids })
|
||||
.groupBy('file.folderId')
|
||||
.getRawMany<{ folderId: string; count: string }>();
|
||||
|
||||
foldersCountMap = new Map(folderCounts.map(row => [row.parentId, Number(row.count)]));
|
||||
filesCountMap = new Map(fileCounts.map(row => [row.folderId, Number(row.count)]));
|
||||
} else {
|
||||
foldersCountMap = new Map();
|
||||
filesCountMap = new Map();
|
||||
}
|
||||
}
|
||||
|
||||
const packedMap = new Map<string, Promise<Packed<'DriveFolder'>>>();
|
||||
const packFromId = (id: string): Promise<Packed<'DriveFolder'>> => {
|
||||
const cached = packedMap.get(id);
|
||||
if (cached) return cached;
|
||||
|
||||
const folder = folderMap.get(id);
|
||||
if (!folder) {
|
||||
throw new Error(`DriveFolder not found: ${id}`);
|
||||
}
|
||||
|
||||
const packedPromise = this.pack(folder, options, {
|
||||
folderMap,
|
||||
foldersCountMap,
|
||||
filesCountMap,
|
||||
parentPacker: packFromId,
|
||||
});
|
||||
packedMap.set(id, packedPromise);
|
||||
|
||||
return packedPromise;
|
||||
};
|
||||
|
||||
return Promise.all(src.map(s => packFromId(typeof s === 'string' ? s : s.id)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* idとオブジェクトを分離する
|
||||
* @param input idまたはオブジェクトの配列
|
||||
* @returns idの配列とオブジェクトの配列
|
||||
*/
|
||||
export function splitIdAndObjects<T extends { id: string }>(input: (T | string)[]): { ids: string[]; objects: T[] } {
|
||||
const ids: string[] = [];
|
||||
const objects : T[] = [];
|
||||
|
||||
for (const item of input) {
|
||||
if (typeof item === 'string') {
|
||||
ids.push(item);
|
||||
} else {
|
||||
objects.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ids,
|
||||
objects,
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* itemsの中でkey関数が返す値が重複しないようにした配列を返す
|
||||
* @param items 重複を除去したい配列
|
||||
* @param key 重複判定に使うキーを返す関数
|
||||
* @returns 重複を除去した配列
|
||||
*/
|
||||
export function uniqueByKey<TItem, TKey = string>(items: Iterable<TItem>, key: (item: TItem) => TKey): TItem[] {
|
||||
const map = new Map<TKey, TItem>();
|
||||
for (const item of items) {
|
||||
const k = key(item);
|
||||
if (!map.has(k)) {
|
||||
map.set(k, item);
|
||||
}
|
||||
}
|
||||
return [...map.values()];
|
||||
}
|
||||
|
|
@ -4,8 +4,9 @@
|
|||
*/
|
||||
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { dirname } from 'node:path';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as fs from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import ms from 'ms';
|
||||
import sharp from 'sharp';
|
||||
|
|
@ -69,13 +70,28 @@ import type { FastifyError, FastifyInstance, FastifyPluginOptions, FastifyReply
|
|||
const _filename = fileURLToPath(import.meta.url);
|
||||
const _dirname = dirname(_filename);
|
||||
|
||||
const staticAssets = `${_dirname}/../../../assets/`;
|
||||
const clientAssets = `${_dirname}/../../../../frontend/assets/`;
|
||||
const assets = `${_dirname}/../../../../../built/_frontend_dist_/`;
|
||||
const swAssets = `${_dirname}/../../../../../built/_sw_dist_/`;
|
||||
const frontendViteOut = `${_dirname}/../../../../../built/_frontend_vite_/`;
|
||||
const frontendEmbedViteOut = `${_dirname}/../../../../../built/_frontend_embed_vite_/`;
|
||||
const tarball = `${_dirname}/../../../../../built/tarball/`;
|
||||
let rootDir = _dirname;
|
||||
// 見つかるまで上に遡る
|
||||
while (!fs.existsSync(resolve(rootDir, 'packages'))) {
|
||||
const parentDir = dirname(rootDir);
|
||||
if (parentDir === rootDir) {
|
||||
throw new Error('Cannot find root directory');
|
||||
}
|
||||
rootDir = parentDir;
|
||||
}
|
||||
|
||||
const backendRootDir = resolve(rootDir, 'packages/backend');
|
||||
const frontendRootDir = resolve(rootDir, 'packages/frontend');
|
||||
|
||||
const staticAssets = resolve(backendRootDir, 'assets');
|
||||
const clientAssets = resolve(frontendRootDir, 'assets');
|
||||
const assets = resolve(rootDir, 'built/_frontend_dist_');
|
||||
const swAssets = resolve(rootDir, 'built/_sw_dist_');
|
||||
const fluentEmojisDir = resolve(rootDir, 'fluent-emojis/dist');
|
||||
const twemojiDir = resolve(backendRootDir, 'node_modules/@discordapp/twemoji/dist/svg');
|
||||
const frontendViteOut = resolve(rootDir, 'built/_frontend_vite_');
|
||||
const frontendEmbedViteOut = resolve(rootDir, 'built/_frontend_embed_vite_');
|
||||
const tarball = resolve(rootDir, 'built/tarball');
|
||||
|
||||
@Injectable()
|
||||
export class ClientServerService {
|
||||
|
|
@ -207,6 +223,7 @@ export class ClientServerService {
|
|||
|
||||
//#region vite assets
|
||||
if (this.config.frontendEmbedManifestExists) {
|
||||
console.log(`[ClientServerService] Using built frontend vite assets. ${frontendViteOut}`);
|
||||
fastify.register((fastify, options, done) => {
|
||||
fastify.register(fastifyStatic, {
|
||||
root: frontendViteOut,
|
||||
|
|
@ -226,6 +243,7 @@ export class ClientServerService {
|
|||
done();
|
||||
});
|
||||
} else {
|
||||
console.log('[ClientServerService] Proxying to Vite dev server.');
|
||||
const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, '');
|
||||
|
||||
const port = (process.env.VITE_PORT ?? '5173');
|
||||
|
|
@ -297,7 +315,7 @@ export class ClientServerService {
|
|||
|
||||
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
||||
|
||||
return await reply.sendFile(path, `${_dirname}/../../../../../fluent-emojis/dist/`, {
|
||||
return reply.sendFile(path, fluentEmojisDir, {
|
||||
maxAge: ms('30 days'),
|
||||
});
|
||||
});
|
||||
|
|
@ -312,7 +330,7 @@ export class ClientServerService {
|
|||
|
||||
reply.header('Content-Security-Policy', 'default-src \'none\'; style-src \'unsafe-inline\'');
|
||||
|
||||
return await reply.sendFile(path, `${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/`, {
|
||||
return reply.sendFile(path, twemojiDir, {
|
||||
maxAge: ms('30 days'),
|
||||
});
|
||||
});
|
||||
|
|
@ -326,7 +344,7 @@ export class ClientServerService {
|
|||
}
|
||||
|
||||
const mask = await sharp(
|
||||
`${_dirname}/../../../node_modules/@discordapp/twemoji/dist/svg/${path.replace('.png', '')}.svg`,
|
||||
`${twemojiDir}/${path.replace('.png', '')}.svg`,
|
||||
{ density: 1000 },
|
||||
)
|
||||
.resize(488, 488)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ services:
|
|||
source: ../built
|
||||
target: /misskey/packages/backend/built
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../src-js
|
||||
target: /misskey/packages/backend/src-js
|
||||
read_only: true
|
||||
- type: bind
|
||||
source: ../migration
|
||||
target: /misskey/packages/backend/migration
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
"experimental": {
|
||||
"keepImportAssertions": true
|
||||
},
|
||||
"baseUrl": "../built",
|
||||
"baseUrl": "../src-js",
|
||||
"paths": {
|
||||
"@/*": ["*"]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, jest, test } from '@jest/globals';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { DriveFilesRepository, DriveFoldersRepository, UsersRepository } from '@/models/_.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { DriveFileEntityService } from '@/core/entities/DriveFileEntityService.js';
|
||||
import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js';
|
||||
import { UserEntityService } from '@/core/entities/UserEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
||||
const describeBenchmark = process.env.RUN_BENCHMARKS === '1' ? describe : describe.skip;
|
||||
|
||||
describe('DriveFileEntityService', () => {
|
||||
let app: TestingModule;
|
||||
let service: DriveFileEntityService;
|
||||
let driveFolderEntityService: DriveFolderEntityService;
|
||||
let driveFilesRepository: DriveFilesRepository;
|
||||
let driveFoldersRepository: DriveFoldersRepository;
|
||||
let usersRepository: UsersRepository;
|
||||
let idCounter = 0;
|
||||
|
||||
const userEntityServiceMock = {
|
||||
packMany: jest.fn(async (users: Array<string | { id: string }>) => {
|
||||
return users.map(u => ({
|
||||
id: typeof u === 'string' ? u : u.id,
|
||||
username: 'user',
|
||||
}));
|
||||
}),
|
||||
pack: jest.fn(async (user: string | { id: string }) => {
|
||||
return {
|
||||
id: typeof user === 'string' ? user : user.id,
|
||||
username: 'user',
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
const nextId = () => genAidx(Date.now() + (idCounter++));
|
||||
|
||||
const createUser = async () => {
|
||||
const un = secureRndstr(16);
|
||||
const id = nextId();
|
||||
await usersRepository.insert({
|
||||
id,
|
||||
username: un,
|
||||
usernameLower: un.toLowerCase(),
|
||||
});
|
||||
return usersRepository.findOneByOrFail({ id });
|
||||
};
|
||||
|
||||
const createFolder = async (name: string, parentId: string | null) => {
|
||||
const id = nextId();
|
||||
await driveFoldersRepository.insert({
|
||||
id,
|
||||
name,
|
||||
userId: null,
|
||||
parentId,
|
||||
});
|
||||
return driveFoldersRepository.findOneByOrFail({ id });
|
||||
};
|
||||
|
||||
const createFile = async (folderId: string | null, userId: string | null) => {
|
||||
const id = nextId();
|
||||
await driveFilesRepository.insert({
|
||||
id,
|
||||
userId,
|
||||
userHost: null,
|
||||
md5: secureRndstr(32),
|
||||
name: `file-${id}`,
|
||||
type: 'text/plain',
|
||||
size: 1,
|
||||
comment: null,
|
||||
blurhash: null,
|
||||
properties: {},
|
||||
storedInternal: true,
|
||||
url: `https://example.com/${id}`,
|
||||
thumbnailUrl: null,
|
||||
webpublicUrl: null,
|
||||
webpublicType: null,
|
||||
accessKey: null,
|
||||
thumbnailAccessKey: null,
|
||||
webpublicAccessKey: null,
|
||||
uri: null,
|
||||
src: null,
|
||||
folderId,
|
||||
isSensitive: false,
|
||||
maybeSensitive: false,
|
||||
maybePorn: false,
|
||||
isLink: false,
|
||||
requestHeaders: null,
|
||||
requestIp: null,
|
||||
});
|
||||
return driveFilesRepository.findOneByOrFail({ id });
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleBuilder = Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule],
|
||||
});
|
||||
moduleBuilder.overrideProvider(UserEntityService).useValue(userEntityServiceMock as any);
|
||||
|
||||
app = await moduleBuilder.compile();
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
service = app.get<DriveFileEntityService>(DriveFileEntityService);
|
||||
driveFolderEntityService = app.get<DriveFolderEntityService>(DriveFolderEntityService);
|
||||
driveFilesRepository = app.get<DriveFilesRepository>(DI.driveFilesRepository);
|
||||
driveFoldersRepository = app.get<DriveFoldersRepository>(DI.driveFoldersRepository);
|
||||
usersRepository = app.get<UsersRepository>(DI.usersRepository);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
userEntityServiceMock.packMany.mockClear();
|
||||
userEntityServiceMock.pack.mockClear();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('pack', () => {
|
||||
test('detail: false', async () => {
|
||||
const user = await createUser();
|
||||
const folder = await createFolder('pack-root', null);
|
||||
const file = await createFile(folder.id, user.id);
|
||||
|
||||
const packed = await service.pack(file, { detail: false, self: true }) as any;
|
||||
expect(packed.id).toBe(file.id);
|
||||
expect(packed.folder).toBeNull();
|
||||
expect(packed.user).toBeNull();
|
||||
expect(packed.userId).toBeNull();
|
||||
});
|
||||
|
||||
test('detail: true', async () => {
|
||||
const folder = await createFolder('pack-parent', null);
|
||||
const child = await createFolder('pack-child', folder.id);
|
||||
const file = await createFile(child.id, null);
|
||||
|
||||
const packed = await service.pack(file, { detail: true, self: true }) as any;
|
||||
expect(packed.folder?.id).toBe(child.id);
|
||||
expect(packed.folder?.parent?.id).toBe(folder.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('packNullable', () => {
|
||||
test('returns null for missing', async () => {
|
||||
const packed = await service.packNullable('non-existent' as any, { detail: false });
|
||||
expect(packed).toBeNull();
|
||||
});
|
||||
|
||||
test('uses packedUser hint when withUser', async () => {
|
||||
const user = await createUser();
|
||||
const file = await createFile(null, user.id);
|
||||
|
||||
const packed = await service.packNullable(file, { withUser: true, self: true }, {
|
||||
packedUser: { id: user.id, username: 'hint' } as any,
|
||||
});
|
||||
expect(packed?.user?.id).toBe(user.id);
|
||||
expect(packed?.user?.username).toBe('hint');
|
||||
});
|
||||
});
|
||||
|
||||
describe('packMany', () => {
|
||||
test('withUser: true uses deduped packMany', async () => {
|
||||
const user = await createUser();
|
||||
const fileA = await createFile(null, user.id);
|
||||
const fileB = await createFile(null, user.id);
|
||||
|
||||
const packed = await service.packMany([fileA, fileB], { withUser: true, self: true });
|
||||
expect(packed.length).toBe(2);
|
||||
expect(userEntityServiceMock.packMany).toHaveBeenCalledTimes(1);
|
||||
expect(userEntityServiceMock.packMany.mock.calls[0]?.[0]?.length).toBe(1);
|
||||
expect(packed[0]?.user?.id).toBe(user.id);
|
||||
});
|
||||
|
||||
test('detail: true packs folder', async () => {
|
||||
const folder = await createFolder('packmany-root', null);
|
||||
const file = await createFile(folder.id, null);
|
||||
|
||||
const packed = await service.packMany([file], { detail: true, self: true });
|
||||
expect(packed[0]?.folder?.id).toBe(folder.id);
|
||||
expect(packed[0]?.folder?.parent).toBeUndefined();
|
||||
});
|
||||
|
||||
test('detail: true uses DriveFolderEntityService pack', async () => {
|
||||
const folder = await createFolder('packmany-folder', null);
|
||||
const file = await createFile(folder.id, null);
|
||||
const packSpy = jest.spyOn(driveFolderEntityService, 'pack');
|
||||
|
||||
await service.packMany([file], { detail: true, self: true });
|
||||
expect(packSpy).toHaveBeenCalled();
|
||||
packSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describeBenchmark('benchmark', () => {
|
||||
test('packMany', async () => {
|
||||
const user = await createUser();
|
||||
const folders = [];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
folders.push(await createFolder(`bench-${i}`, null));
|
||||
}
|
||||
const files = [];
|
||||
for (const folder of folders) {
|
||||
for (let j = 0; j < 20; j++) {
|
||||
files.push(await createFile(folder.id, user.id));
|
||||
}
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
await service.packMany(files, { detail: true, withUser: true, self: true });
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
console.log(`DriveFileEntityService.packMany benchmark: ${elapsed}ms`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
process.env.NODE_ENV = 'test';
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, test } from '@jest/globals';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import type { TestingModule } from '@nestjs/testing';
|
||||
import type { DriveFilesRepository, DriveFoldersRepository } from '@/models/_.js';
|
||||
import { GlobalModule } from '@/GlobalModule.js';
|
||||
import { CoreModule } from '@/core/CoreModule.js';
|
||||
import { DriveFolderEntityService } from '@/core/entities/DriveFolderEntityService.js';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import { genAidx } from '@/misc/id/aidx.js';
|
||||
import { secureRndstr } from '@/misc/secure-rndstr.js';
|
||||
|
||||
const describeBenchmark = process.env.RUN_BENCHMARKS === '1' ? describe : describe.skip;
|
||||
|
||||
describe('DriveFolderEntityService', () => {
|
||||
let app: TestingModule;
|
||||
let service: DriveFolderEntityService;
|
||||
let driveFoldersRepository: DriveFoldersRepository;
|
||||
let driveFilesRepository: DriveFilesRepository;
|
||||
let idCounter = 0;
|
||||
|
||||
const nextId = () => genAidx(Date.now() + (idCounter++));
|
||||
|
||||
const createFolder = async (name: string, parentId: string | null) => {
|
||||
const id = nextId();
|
||||
await driveFoldersRepository.insert({
|
||||
id,
|
||||
name,
|
||||
userId: null,
|
||||
parentId,
|
||||
});
|
||||
return driveFoldersRepository.findOneByOrFail({ id });
|
||||
};
|
||||
|
||||
const createFile = async (folderId: string | null) => {
|
||||
const id = nextId();
|
||||
await driveFilesRepository.insert({
|
||||
id,
|
||||
userId: null,
|
||||
userHost: null,
|
||||
md5: secureRndstr(32),
|
||||
name: `file-${id}`,
|
||||
type: 'text/plain',
|
||||
size: 1,
|
||||
comment: null,
|
||||
blurhash: null,
|
||||
properties: {},
|
||||
storedInternal: true,
|
||||
url: `https://example.com/${id}`,
|
||||
thumbnailUrl: null,
|
||||
webpublicUrl: null,
|
||||
webpublicType: null,
|
||||
accessKey: null,
|
||||
thumbnailAccessKey: null,
|
||||
webpublicAccessKey: null,
|
||||
uri: null,
|
||||
src: null,
|
||||
folderId,
|
||||
isSensitive: false,
|
||||
maybeSensitive: false,
|
||||
maybePorn: false,
|
||||
isLink: false,
|
||||
requestHeaders: null,
|
||||
requestIp: null,
|
||||
});
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await Test.createTestingModule({
|
||||
imports: [GlobalModule, CoreModule],
|
||||
}).compile();
|
||||
await app.init();
|
||||
app.enableShutdownHooks();
|
||||
|
||||
service = app.get<DriveFolderEntityService>(DriveFolderEntityService);
|
||||
driveFoldersRepository = app.get<DriveFoldersRepository>(DI.driveFoldersRepository);
|
||||
driveFilesRepository = app.get<DriveFilesRepository>(DI.driveFilesRepository);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await app.close();
|
||||
});
|
||||
|
||||
describe('pack', () => {
|
||||
test('detail: false', async () => {
|
||||
const root = await createFolder('root', null);
|
||||
const child = await createFolder('child', root.id);
|
||||
|
||||
const packed = await service.pack(child, { detail: false }) as any;
|
||||
expect(packed.id).toBe(child.id);
|
||||
expect(packed.parentId).toBe(root.id);
|
||||
expect(packed.parent).toBeUndefined();
|
||||
expect(packed.foldersCount).toBeUndefined();
|
||||
expect(packed.filesCount).toBeUndefined();
|
||||
});
|
||||
|
||||
test('detail: true', async () => {
|
||||
const root = await createFolder('root-detail', null);
|
||||
const child = await createFolder('child-detail', root.id);
|
||||
await createFolder('grandchild-detail', child.id);
|
||||
await createFile(child.id);
|
||||
await createFile(child.id);
|
||||
|
||||
const packed = await service.pack(child, { detail: true }) as any;
|
||||
expect(packed.id).toBe(child.id);
|
||||
expect(packed.foldersCount).toBe(1);
|
||||
expect(packed.filesCount).toBe(2);
|
||||
expect(packed.parent?.id).toBe(root.id);
|
||||
expect(packed.parent?.parent).toBeUndefined();
|
||||
});
|
||||
|
||||
test('detail: true reaches root for deep hierarchy', async () => {
|
||||
const root = await createFolder('root-deep', null);
|
||||
const level1 = await createFolder('level-1', root.id);
|
||||
const level2 = await createFolder('level-2', level1.id);
|
||||
const level3 = await createFolder('level-3', level2.id);
|
||||
const level4 = await createFolder('level-4', level3.id);
|
||||
const level5 = await createFolder('level-5', level4.id);
|
||||
|
||||
const packed = await service.pack(level5, { detail: true }) as any;
|
||||
expect(packed.id).toBe(level5.id);
|
||||
expect(packed.parent?.id).toBe(level4.id);
|
||||
expect(packed.parent?.parent?.id).toBe(level3.id);
|
||||
expect(packed.parent?.parent?.parent?.id).toBe(level2.id);
|
||||
expect(packed.parent?.parent?.parent?.parent?.id).toBe(level1.id);
|
||||
expect(packed.parent?.parent?.parent?.parent?.parent?.id).toBe(root.id);
|
||||
expect(packed.parent?.parent?.parent?.parent?.parent?.parent).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('packMany', () => {
|
||||
test('preserves order and packs parents', async () => {
|
||||
const root = await createFolder('root-many', null);
|
||||
const childA = await createFolder('child-a', root.id);
|
||||
const childB = await createFolder('child-b', root.id);
|
||||
await createFolder('child-a-sub', childA.id);
|
||||
await createFile(childA.id);
|
||||
|
||||
const packed = await service.packMany([childB, childA], { detail: true }) as any;
|
||||
expect(packed[0].id).toBe(childB.id);
|
||||
expect(packed[1].id).toBe(childA.id);
|
||||
expect(packed[0].parent?.id).toBe(root.id);
|
||||
expect(packed[1].parent?.id).toBe(root.id);
|
||||
expect(packed[0].filesCount).toBe(0);
|
||||
expect(packed[1].filesCount).toBe(1);
|
||||
expect(packed[0].foldersCount).toBe(0);
|
||||
expect(packed[1].foldersCount).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describeBenchmark('benchmark', () => {
|
||||
test('packMany', async () => {
|
||||
const root = await createFolder('bench-root', null);
|
||||
const folders = [];
|
||||
for (let i = 0; i < 200; i++) {
|
||||
folders.push(await createFolder(`bench-${i}`, root.id));
|
||||
}
|
||||
|
||||
const start = Date.now();
|
||||
await service.packMany(folders, { detail: true });
|
||||
const elapsed = Date.now() - start;
|
||||
console.log(`DriveFolderEntityService.packMany benchmark: ${elapsed}ms`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -11,15 +11,15 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/node": "24.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"rollup": "4.53.3"
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"rollup": "4.54.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"estree-walker": "3.0.3",
|
||||
"magic-string": "0.30.21",
|
||||
"vite": "7.2.7"
|
||||
"vite": "7.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
"@rollup/plugin-replace": "6.0.3",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@vitejs/plugin-vue": "6.0.2",
|
||||
"@vitejs/plugin-vue": "6.0.3",
|
||||
"buraha": "0.0.1",
|
||||
"estree-walker": "3.0.3",
|
||||
"frontend-shared": "workspace:*",
|
||||
|
|
@ -25,13 +25,13 @@
|
|||
"mfm-js": "0.25.0",
|
||||
"misskey-js": "workspace:*",
|
||||
"punycode.js": "2.3.1",
|
||||
"rollup": "4.53.3",
|
||||
"sass": "1.95.1",
|
||||
"shiki": "3.19.0",
|
||||
"rollup": "4.54.0",
|
||||
"sass": "1.97.1",
|
||||
"shiki": "3.20.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"uuid": "13.0.0",
|
||||
"vite": "7.2.7",
|
||||
"vue": "3.5.25"
|
||||
"vite": "7.3.0",
|
||||
"vue": "3.5.26"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
|
|
@ -39,14 +39,14 @@
|
|||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/micromatch": "4.0.10",
|
||||
"@types/node": "24.10.2",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@types/ws": "8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"@vitest/coverage-v8": "4.0.15",
|
||||
"@vue/runtime-core": "3.5.25",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"@vue/runtime-core": "3.5.26",
|
||||
"acorn": "8.15.0",
|
||||
"cross-env": "10.1.0",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
|
|
@ -54,13 +54,13 @@
|
|||
"happy-dom": "20.0.11",
|
||||
"intersection-observer": "0.12.2",
|
||||
"micromatch": "4.0.8",
|
||||
"msw": "2.12.4",
|
||||
"msw": "2.12.6",
|
||||
"nodemon": "3.1.11",
|
||||
"prettier": "3.7.4",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"tsx": "4.21.0",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vue-component-type-helpers": "3.1.8",
|
||||
"vue-component-type-helpers": "3.2.1",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.1.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,10 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"esbuild": "0.27.1",
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"esbuild": "0.27.2",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"nodemon": "3.1.11",
|
||||
"vue-eslint-parser": "10.2.0"
|
||||
|
|
@ -35,6 +35,6 @@
|
|||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
"vue": "3.5.25"
|
||||
"vue": "3.5.26"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@
|
|||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/plugin-replace": "6.0.3",
|
||||
"@rollup/pluginutils": "5.3.0",
|
||||
"@sentry/vue": "10.29.0",
|
||||
"@syuilo/aiscript": "1.2.0",
|
||||
"@sentry/vue": "10.32.1",
|
||||
"@syuilo/aiscript": "1.2.1",
|
||||
"@syuilo/aiscript-0-19-0": "npm:@syuilo/aiscript@^0.19.0",
|
||||
"@twemoji/parser": "16.0.0",
|
||||
"@vitejs/plugin-vue": "6.0.2",
|
||||
"@vitejs/plugin-vue": "6.0.3",
|
||||
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.16",
|
||||
"analytics": "0.8.19",
|
||||
"broadcast-channel": "7.2.0",
|
||||
|
|
@ -55,7 +55,7 @@
|
|||
"is-file-animated": "1.0.2",
|
||||
"json5": "2.2.3",
|
||||
"matter-js": "0.20.0",
|
||||
"mediabunny": "1.25.8",
|
||||
"mediabunny": "1.27.2",
|
||||
"mfm-js": "0.25.0",
|
||||
"misskey-bubble-game": "workspace:*",
|
||||
"misskey-js": "workspace:*",
|
||||
|
|
@ -64,59 +64,58 @@
|
|||
"punycode.js": "2.3.1",
|
||||
"qr-code-styling": "1.9.2",
|
||||
"qr-scanner": "1.4.2",
|
||||
"rollup": "4.53.3",
|
||||
"rollup": "4.54.0",
|
||||
"sanitize-html": "2.17.0",
|
||||
"sass": "1.95.1",
|
||||
"shiki": "3.19.0",
|
||||
"sass": "1.97.1",
|
||||
"shiki": "3.20.0",
|
||||
"textarea-caret": "3.1.0",
|
||||
"three": "0.181.2",
|
||||
"three": "0.182.0",
|
||||
"throttle-debounce": "5.0.2",
|
||||
"tinycolor2": "1.6.0",
|
||||
"v-code-diff": "1.13.1",
|
||||
"vite": "7.2.7",
|
||||
"vue": "3.5.25",
|
||||
"vuedraggable": "next",
|
||||
"vite": "7.3.0",
|
||||
"vue": "3.5.26",
|
||||
"wanakana": "5.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/summaly": "5.2.5",
|
||||
"@storybook/addon-essentials": "8.6.14",
|
||||
"@storybook/addon-interactions": "8.6.14",
|
||||
"@storybook/addon-links": "10.1.5",
|
||||
"@storybook/addon-mdx-gfm": "8.6.14",
|
||||
"@storybook/addon-storysource": "8.6.14",
|
||||
"@storybook/blocks": "8.6.14",
|
||||
"@storybook/components": "8.6.14",
|
||||
"@storybook/core-events": "8.6.14",
|
||||
"@storybook/manager-api": "8.6.14",
|
||||
"@storybook/preview-api": "8.6.14",
|
||||
"@storybook/react": "10.1.5",
|
||||
"@storybook/react-vite": "10.1.5",
|
||||
"@storybook/test": "8.6.14",
|
||||
"@storybook/theming": "8.6.14",
|
||||
"@storybook/types": "8.6.14",
|
||||
"@storybook/vue3": "10.1.5",
|
||||
"@storybook/vue3-vite": "10.1.5",
|
||||
"@storybook/addon-essentials": "8.6.15",
|
||||
"@storybook/addon-interactions": "8.6.15",
|
||||
"@storybook/addon-links": "10.1.10",
|
||||
"@storybook/addon-mdx-gfm": "8.6.15",
|
||||
"@storybook/addon-storysource": "8.6.15",
|
||||
"@storybook/blocks": "8.6.15",
|
||||
"@storybook/components": "8.6.15",
|
||||
"@storybook/core-events": "8.6.15",
|
||||
"@storybook/manager-api": "8.6.15",
|
||||
"@storybook/preview-api": "8.6.15",
|
||||
"@storybook/react": "10.1.10",
|
||||
"@storybook/react-vite": "10.1.10",
|
||||
"@storybook/test": "8.6.15",
|
||||
"@storybook/theming": "8.6.15",
|
||||
"@storybook/types": "8.6.15",
|
||||
"@storybook/vue3": "10.1.10",
|
||||
"@storybook/vue3-vite": "10.1.10",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/canvas-confetti": "1.9.0",
|
||||
"@types/estree": "1.0.8",
|
||||
"@types/matter-js": "0.20.2",
|
||||
"@types/micromatch": "4.0.10",
|
||||
"@types/node": "24.10.2",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/punycode.js": "npm:@types/punycode@2.1.4",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@types/throttle-debounce": "5.0.2",
|
||||
"@types/tinycolor2": "1.4.6",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"@vitest/coverage-v8": "4.0.15",
|
||||
"@vue/compiler-core": "3.5.25",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"@vue/compiler-core": "3.5.26",
|
||||
"acorn": "8.15.0",
|
||||
"astring": "1.9.0",
|
||||
"cross-env": "10.1.0",
|
||||
"cypress": "15.7.1",
|
||||
"cypress": "15.8.1",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-vue": "10.6.2",
|
||||
"estree-walker": "3.0.3",
|
||||
|
|
@ -125,22 +124,22 @@
|
|||
"magic-string": "0.30.21",
|
||||
"micromatch": "4.0.8",
|
||||
"minimatch": "10.1.1",
|
||||
"msw": "2.12.4",
|
||||
"msw": "2.12.6",
|
||||
"msw-storybook-addon": "2.0.6",
|
||||
"nodemon": "3.1.11",
|
||||
"prettier": "3.7.4",
|
||||
"react": "19.2.1",
|
||||
"react-dom": "19.2.1",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"seedrandom": "3.0.5",
|
||||
"start-server-and-test": "2.1.3",
|
||||
"storybook": "10.1.5",
|
||||
"storybook": "10.1.10",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"tsx": "4.21.0",
|
||||
"vite-plugin-glsl": "1.5.5",
|
||||
"vite-plugin-turbosnap": "1.0.3",
|
||||
"vitest": "4.0.15",
|
||||
"vitest": "4.0.16",
|
||||
"vitest-fetch-mock": "0.4.5",
|
||||
"vue-component-type-helpers": "3.1.8",
|
||||
"vue-component-type-helpers": "3.2.1",
|
||||
"vue-eslint-parser": "10.2.0",
|
||||
"vue-tsc": "3.1.8"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,310 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<TransitionGroup
|
||||
tag="div"
|
||||
:enterActiveClass="$style.transition_items_enterActive"
|
||||
:leaveActiveClass="$style.transition_items_leaveActive"
|
||||
:enterFromClass="$style.transition_items_enterFrom"
|
||||
:leaveToClass="$style.transition_items_leaveTo"
|
||||
:moveClass="$style.transition_items_move"
|
||||
:class="[$style.items, { [$style.dragging]: dragging, [$style.horizontal]: direction === 'horizontal', [$style.vertical]: direction === 'vertical', [$style.withGaps]: withGaps, [$style.canNest]: canNest }]"
|
||||
>
|
||||
<slot name="header"></slot>
|
||||
<div
|
||||
v-if="modelValue.length === 0"
|
||||
:class="$style.emptyDropArea"
|
||||
@dragover.prevent.stop="() => {}"
|
||||
@dragleave="() => {}"
|
||||
@drop.prevent.stop="onEmptyDrop($event)"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-for="(item, i) in modelValue"
|
||||
:key="item.id"
|
||||
:class="$style.item"
|
||||
:draggable="!manualDragStart"
|
||||
@dragstart.stop="onDragstart($event, item)"
|
||||
>
|
||||
<div
|
||||
:class="[$style.forwardArea, { [$style.dropReady]: dropReadyArea[0] === item.id && dropReadyArea[1] === 'forward' }]"
|
||||
@dragover.prevent.stop="onDragover($event, item, false)"
|
||||
@dragleave="onDragleave($event, item)"
|
||||
@drop.prevent.stop="onDrop($event, item, false)"
|
||||
></div>
|
||||
<div style="position: relative; z-index: 0;">
|
||||
<slot :item="item" :index="i" :dragStart="(ev) => onDragstart(ev, item)"></slot>
|
||||
</div>
|
||||
<div
|
||||
:class="[$style.backwardArea, { [$style.dropReady]: dropReadyArea[0] === item.id && dropReadyArea[1] === 'backward' }]"
|
||||
@dragover.prevent.stop="onDragover($event, item, true)"
|
||||
@dragleave="onDragleave($event, item)"
|
||||
@drop.prevent.stop="onDrop($event, item, true)"
|
||||
></div>
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</TransitionGroup>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
// 別々のコンポーネントインスタンス間でD&Dを融通するためにグローバルに状態を持っておく必要がある
|
||||
const dragging = ref(false);
|
||||
let dropCallback: ((targetInstanceId: string) => void) | null = null;
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup generic="T extends { id: string; }">
|
||||
import { nextTick } from 'vue';
|
||||
import { getDragData, setDragData } from '@/drag-and-drop.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
|
||||
const slots = defineSlots<{
|
||||
default(props: { item: T; index: number; dragStart: (ev: DragEvent) => void }): any;
|
||||
header(): any;
|
||||
footer(): any;
|
||||
}>();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: T[];
|
||||
direction: 'horizontal' | 'vertical';
|
||||
group?: string | null;
|
||||
manualDragStart?: boolean;
|
||||
withGaps?: boolean;
|
||||
canNest?: boolean;
|
||||
}>(), {
|
||||
group: null,
|
||||
manualDragStart: false,
|
||||
withGaps: false,
|
||||
canNest: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: T[]): void;
|
||||
}>();
|
||||
|
||||
const dropReadyArea = ref<[T['id'] | null, 'forward' | 'backward' | null]>([null, null]);
|
||||
const instanceId = genId();
|
||||
const group = props.group ?? instanceId;
|
||||
|
||||
function onDragstart(ev: DragEvent, item: T) {
|
||||
if (ev.dataTransfer == null) return;
|
||||
ev.dataTransfer.effectAllowed = 'move';
|
||||
setDragData(ev, 'MkDraggable', { item, instanceId, group });
|
||||
|
||||
const target = ev.target as HTMLElement;
|
||||
target.addEventListener('dragend', (ev) => {
|
||||
dragging.value = false;
|
||||
dropReadyArea.value = [null, null];
|
||||
}, { once: true });
|
||||
|
||||
dropCallback = (targetInstanceId) => {
|
||||
if (targetInstanceId === instanceId) return;
|
||||
const newValue = props.modelValue.filter(x => x.id !== item.id);
|
||||
emit('update:modelValue', newValue);
|
||||
};
|
||||
|
||||
// Chromeのバグで、Dragstartハンドラ内ですぐにDOMを変更する(=リアクティブなプロパティを変更する)とDragが終了してしまう
|
||||
// SEE: https://stackoverflow.com/questions/19639969/html5-dragend-event-firing-immediately
|
||||
window.setTimeout(() => {
|
||||
dragging.value = true;
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function onDragover(ev: DragEvent, item: T, backward: boolean) {
|
||||
nextTick(() => {
|
||||
dropReadyArea.value = [item.id, backward ? 'backward' : 'forward'];
|
||||
});
|
||||
}
|
||||
|
||||
function onDragleave(ev: DragEvent, item: T) {
|
||||
dropReadyArea.value = [null, null];
|
||||
}
|
||||
|
||||
function onDrop(ev: DragEvent, item: T, backward: boolean) {
|
||||
const dragged = getDragData(ev, 'MkDraggable');
|
||||
dropReadyArea.value = [null, null];
|
||||
if (dragged == null || dragged.group !== group || dragged.item.id === item.id) return;
|
||||
dropCallback?.(instanceId);
|
||||
|
||||
const fromIndex = props.modelValue.findIndex(x => x.id === dragged.item.id);
|
||||
let toIndex = props.modelValue.findIndex(x => x.id === item.id);
|
||||
|
||||
const newValue = [...props.modelValue];
|
||||
if (fromIndex > -1) newValue.splice(fromIndex, 1);
|
||||
toIndex = newValue.findIndex(x => x.id === item.id);
|
||||
if (backward) toIndex += 1;
|
||||
newValue.splice(toIndex, 0, dragged.item as T);
|
||||
|
||||
emit('update:modelValue', newValue);
|
||||
}
|
||||
|
||||
function onEmptyDrop(ev: DragEvent) {
|
||||
const dragged = getDragData(ev, 'MkDraggable');
|
||||
if (dragged == null) return;
|
||||
dropCallback?.(instanceId);
|
||||
|
||||
emit('update:modelValue', [dragged.item as T]);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_items_move,
|
||||
.transition_items_enterActive,
|
||||
.transition_items_leaveActive {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
.transition_items_enterFrom,
|
||||
.transition_items_leaveTo {
|
||||
opacity: 0;
|
||||
}
|
||||
.transition_items_leaveActive {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: left;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items.horizontal {
|
||||
flex-direction: row;
|
||||
}
|
||||
.items.vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.items.vertical .item {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.items.horizontal.withGaps {
|
||||
row-gap: var(--MI-margin);
|
||||
}
|
||||
|
||||
.items.horizontal.withGaps .item {
|
||||
padding-left: calc(var(--MI-margin) / 2);
|
||||
padding-right: calc(var(--MI-margin) / 2);
|
||||
}
|
||||
|
||||
.items.vertical.withGaps .item {
|
||||
padding-top: calc(var(--MI-margin) / 2);
|
||||
padding-bottom: calc(var(--MI-margin) / 2);
|
||||
}
|
||||
|
||||
.forwardArea, .backwardArea {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.items.dragging {
|
||||
.forwardArea, .backwardArea {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.items.horizontal {
|
||||
.forwardArea {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.backwardArea {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.items.vertical {
|
||||
.forwardArea {
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
|
||||
.backwardArea {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.items.canNest.horizontal {
|
||||
.forwardArea, .backwardArea {
|
||||
width: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.items.canNest.vertical {
|
||||
.forwardArea, .backwardArea {
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropReady::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 99999;
|
||||
background: var(--MI_THEME-accent);
|
||||
border-radius: 999px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.items.horizontal {
|
||||
.forwardArea.dropReady::before {
|
||||
top: 0;
|
||||
left: -1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.backwardArea.dropReady::before {
|
||||
top: 0;
|
||||
right: -1px;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.items.vertical {
|
||||
.forwardArea.dropReady::before {
|
||||
top: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
|
||||
.backwardArea.dropReady::before {
|
||||
bottom: -1px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.items.horizontal .emptyDropArea {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.items.vertical .emptyDropArea {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -5,23 +5,30 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<div v-show="props.modelValue.length != 0" :class="$style.root">
|
||||
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{ element }">
|
||||
<MkDraggable
|
||||
:modelValue="props.modelValue"
|
||||
:class="$style.files"
|
||||
direction="horizontal"
|
||||
withGaps
|
||||
@update:modelValue="v => emit('update:modelValue', v)"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
:class="$style.file"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
@click="showFileMenu(element, $event)"
|
||||
@keydown.space.enter="showFileMenu(element, $event)"
|
||||
@contextmenu.prevent="showFileMenu(element, $event)"
|
||||
@click="showFileMenu(item, $event)"
|
||||
@keydown.space.enter="showFileMenu(item, $event)"
|
||||
@contextmenu.prevent="showFileMenu(item, $event)"
|
||||
>
|
||||
<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
|
||||
<div v-if="element.isSensitive" :class="$style.sensitive">
|
||||
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
|
||||
<MkDriveFileThumbnail style="pointer-events: none;" :data-id="item.id" :class="$style.thumbnail" :file="item" fit="cover"/>
|
||||
<div v-if="item.isSensitive" :class="$style.sensitive" style="pointer-events: none;">
|
||||
<i class="ti ti-eye-exclamation" style="margin: auto;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
<p
|
||||
:class="[$style.remain, {
|
||||
[$style.exceeded]: props.modelValue.length > 16,
|
||||
|
|
@ -33,11 +40,12 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, inject } from 'vue';
|
||||
import { inject } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { MenuItem } from '@/types/menu';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -45,8 +53,6 @@ import { prefer } from '@/preferences.js';
|
|||
import { DI } from '@/di.js';
|
||||
import { globalEvents } from '@/events.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Misskey.entities.DriveFile[];
|
||||
detachMediaFn?: (id: string) => void;
|
||||
|
|
@ -221,7 +227,6 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
|||
position: relative;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-right: 4px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
cursor: move;
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.root">
|
||||
<div :class="$style.root" class="_gaps_s">
|
||||
<template v-if="edit">
|
||||
<header :class="$style.editHeader">
|
||||
<MkSelect v-model="widgetAdderSelected" :items="widgetAdderSelectedDef" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
|
||||
|
|
@ -13,25 +13,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
|
||||
</header>
|
||||
<Sortable
|
||||
<MkDraggable
|
||||
:modelValue="props.widgets"
|
||||
itemKey="id"
|
||||
handle=".handle"
|
||||
:animation="150"
|
||||
:group="{ name: 'SortableMkWidgets' }"
|
||||
:class="$style.editEditing"
|
||||
direction="vertical"
|
||||
withGaps
|
||||
group="MkWidgets"
|
||||
@update:modelValue="v => emit('updateWidgets', v)"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<template #default="{ item }">
|
||||
<div :class="[$style.widget, $style.customizeContainer]" data-cy-customize-container>
|
||||
<button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(element.id)"><i class="ti ti-settings"></i></button>
|
||||
<button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(element)"><i class="ti ti-x"></i></button>
|
||||
<div class="handle">
|
||||
<component :is="`widget-${element.name}`" :ref="el => widgetRefs[element.id] = el" class="widget" :class="$style.customizeContainerHandleWidget" :widget="element" @updateProps="updateWidget(element.id, $event)"/>
|
||||
</div>
|
||||
<button :class="$style.customizeContainerConfig" class="_button" @click.prevent.stop="configWidget(item.id)"><i class="ti ti-settings"></i></button>
|
||||
<button :class="$style.customizeContainerRemove" data-cy-customize-container-remove class="_button" @click.prevent.stop="removeWidget(item)"><i class="ti ti-x"></i></button>
|
||||
<component :is="`widget-${item.name}`" :ref="el => widgetRefs[item.id] = el" :class="$style.customizeContainerHandleWidget" :widget="item" @updateProps="updateWidget(item.id, $event)"/>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
</template>
|
||||
<component :is="`widget-${widget.name}`" v-for="widget in _widgets" v-else :key="widget.id" :ref="el => widgetRefs[widget.id] = el" :class="$style.widget" :widget="widget" @updateProps="updateWidget(widget.id, $event)" @contextmenu.stop="onContextmenu(widget, $event)"/>
|
||||
</div>
|
||||
|
|
@ -49,19 +45,18 @@ export type DefaultStoredWidget = {
|
|||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
||||
import { computed } from 'vue';
|
||||
import { isLink } from '@@/js/is-link.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import { widgets as widgetDefs, federationWidgets } from '@/widgets/index.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { useMkSelect } from '@/composables/use-mkselect.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const props = defineProps<{
|
||||
widgets: Widget[];
|
||||
edit: boolean;
|
||||
|
|
@ -142,11 +137,6 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
|||
|
||||
.widget {
|
||||
contain: content;
|
||||
margin: var(--MI-margin) 0;
|
||||
|
||||
&:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.edit {
|
||||
|
|
@ -158,10 +148,6 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
|
|||
padding: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&Editing {
|
||||
min-height: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.customizeContainer {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ type DragDataMap = {
|
|||
driveFiles: Misskey.entities.DriveFile[];
|
||||
driveFolders: Misskey.entities.DriveFolder[];
|
||||
deckColumn: string;
|
||||
MkDraggable: { item: { id: string }; instanceId: string; group: string; };
|
||||
};
|
||||
|
||||
// NOTE: dataTransfer の format は大文字小文字区別されないっぽいので toLowerCase が必要
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.header">
|
||||
<MkSelect v-model="type" :items="typeDef" :class="$style.typeSelect">
|
||||
</MkSelect>
|
||||
<button v-if="draggable" class="drag-handle _button" :class="$style.dragHandle">
|
||||
<button v-if="draggable" class="_button" :class="$style.dragHandle" :draggable="true" @dragstart.stop="dragStartCallback">
|
||||
<i class="ti ti-menu-2"></i>
|
||||
</button>
|
||||
<button v-if="draggable" class="_button" :class="$style.remove" @click="removeSelf">
|
||||
|
|
@ -17,14 +17,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
|
||||
<div v-if="type === 'and' || type === 'or'" class="_gaps">
|
||||
<Sortable v-model="v.values" tag="div" class="_gaps" itemKey="id" handle=".drag-handle" :group="{ name: 'roleFormula' }" :animation="150" :swapThreshold="0.5">
|
||||
<template #item="{element}">
|
||||
<MkDraggable
|
||||
v-model="v.values"
|
||||
direction="vertical"
|
||||
withGaps
|
||||
canNest
|
||||
manualDragStart
|
||||
group="roleFormula"
|
||||
>
|
||||
<template #default="{ item, dragStart }">
|
||||
<div :class="$style.item">
|
||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||
<RolesEditorFormula :modelValue="element" draggable @update:modelValue="updated => valuesItemUpdated(updated)" @remove="removeItem(element)"/>
|
||||
<RolesEditorFormula
|
||||
:modelValue="item"
|
||||
:dragStartCallback="dragStart"
|
||||
draggable
|
||||
@update:modelValue="updated => valuesItemUpdated(updated)"
|
||||
@remove="removeItem(item.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
<MkButton rounded style="margin: 0 auto;" @click="addValue"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
</div>
|
||||
|
||||
|
|
@ -45,18 +58,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSelect from '@/components/MkSelect.vue';
|
||||
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { deepClone } from '@/utility/clone.js';
|
||||
import { rolesCache } from '@/cache.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'update:modelValue', value: any): void;
|
||||
(ev: 'remove'): void;
|
||||
|
|
@ -65,6 +77,7 @@ const emit = defineEmits<{
|
|||
const props = defineProps<{
|
||||
modelValue: any;
|
||||
draggable?: boolean;
|
||||
dragStartCallback?: (ev: DragEvent) => void;
|
||||
}>();
|
||||
|
||||
const v = ref(deepClone(props.modelValue));
|
||||
|
|
@ -132,8 +145,8 @@ function valuesItemUpdated(item) {
|
|||
v.value.values[i] = item;
|
||||
}
|
||||
|
||||
function removeItem(item) {
|
||||
v.value.values = v.value.values.filter(_item => _item.id !== item.id);
|
||||
function removeItem(itemId) {
|
||||
v.value.values = v.value.values.filter(_item => _item.id !== itemId);
|
||||
}
|
||||
|
||||
function removeSelf() {
|
||||
|
|
|
|||
|
|
@ -12,28 +12,25 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps_m">
|
||||
<div><SearchText>{{ i18n.ts._serverRules.description }}</SearchText></div>
|
||||
|
||||
<Sortable
|
||||
<MkDraggable
|
||||
v-model="serverRules"
|
||||
class="_gaps_m"
|
||||
:itemKey="(_, i) => i"
|
||||
:animation="150"
|
||||
:handle="'.' + $style.itemHandle"
|
||||
@start="e => e.item.classList.add('active')"
|
||||
@end="e => e.item.classList.remove('active')"
|
||||
direction="vertical"
|
||||
withGaps
|
||||
manualDragStart
|
||||
>
|
||||
<template #item="{element,index}">
|
||||
<template #default="{ item, index, dragStart }">
|
||||
<div :class="$style.item">
|
||||
<div :class="$style.itemHeader">
|
||||
<div :class="$style.itemNumber" v-text="String(index + 1)"/>
|
||||
<span :class="$style.itemHandle"><i class="ti ti-menu"/></span>
|
||||
<button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ti ti-x"></i></button>
|
||||
<span :class="$style.itemHandle" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"/></span>
|
||||
<button class="_button" :class="$style.itemRemove" @click="remove(item.id)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<MkInput v-model="serverRules[index]"/>
|
||||
<MkInput :modelValue="item.text" @update:modelValue="serverRules[index].text = $event"/>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
<div :class="$style.commands">
|
||||
<MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton rounded @click="add"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton primary rounded @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -42,28 +39,31 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, ref, computed } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import * as os from '@/os.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
const serverRules = ref<{ text: string; id: string; }[]>(instance.serverRules.map(text => ({ text, id: Math.random().toString() })));
|
||||
|
||||
const serverRules = ref<string[]>(instance.serverRules);
|
||||
|
||||
const save = async () => {
|
||||
async function save() {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
serverRules: serverRules.value,
|
||||
serverRules: serverRules.value.map(r => r.text),
|
||||
});
|
||||
fetchInstance(true);
|
||||
};
|
||||
}
|
||||
|
||||
const remove = (index: number): void => {
|
||||
serverRules.value.splice(index, 1);
|
||||
};
|
||||
function add(): void {
|
||||
serverRules.value.push({ text: '', id: Math.random().toString() });
|
||||
}
|
||||
|
||||
function remove(id: string): void {
|
||||
serverRules.value = serverRules.value.filter(r => r.id !== id);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
|||
|
|
@ -41,20 +41,19 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div class="_gaps">
|
||||
<MkButton primary rounded @click="addPinnedNote()"><i class="ti ti-plus"></i></MkButton>
|
||||
|
||||
<Sortable
|
||||
v-model="pinnedNotes"
|
||||
itemKey="id"
|
||||
:handle="'.' + $style.pinnedNoteHandle"
|
||||
:animation="150"
|
||||
<MkDraggable
|
||||
:modelValue="pinnedNoteIds.map(id => ({ id }))"
|
||||
direction="vertical"
|
||||
@update:modelValue="v => pinnedNoteIds = v.map(x => x.id)"
|
||||
>
|
||||
<template #item="{element,index}">
|
||||
<template #default="{ item }">
|
||||
<div :class="$style.pinnedNote">
|
||||
<button class="_button" :class="$style.pinnedNoteHandle"><i class="ti ti-menu"></i></button>
|
||||
{{ element.id }}
|
||||
<button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(index)"><i class="ti ti-x"></i></button>
|
||||
{{ item.id }}
|
||||
<button class="_button" :class="$style.pinnedNoteRemove" @click="removePinnedNote(item.id)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
|
@ -68,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch, defineAsyncComponent } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
|
@ -81,10 +80,9 @@ import { i18n } from '@/i18n.js';
|
|||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import { useRouter } from '@/router.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
@ -99,7 +97,7 @@ const bannerId = ref<string | null>(null);
|
|||
const color = ref('#000');
|
||||
const isSensitive = ref(false);
|
||||
const allowRenoteToExternal = ref(true);
|
||||
const pinnedNotes = ref<{ id: Misskey.entities.Note['id'] }[]>([]);
|
||||
const pinnedNoteIds = ref<Misskey.entities.Note['id'][]>([]);
|
||||
|
||||
watch(() => bannerId.value, async () => {
|
||||
if (bannerId.value == null) {
|
||||
|
|
@ -123,9 +121,7 @@ async function fetchChannel() {
|
|||
bannerId.value = result.bannerId;
|
||||
bannerUrl.value = result.bannerUrl;
|
||||
isSensitive.value = result.isSensitive;
|
||||
pinnedNotes.value = result.pinnedNoteIds.map(id => ({
|
||||
id,
|
||||
}));
|
||||
pinnedNoteIds.value = result.pinnedNoteIds;
|
||||
color.value = result.color;
|
||||
allowRenoteToExternal.value = result.allowRenoteToExternal;
|
||||
|
||||
|
|
@ -143,13 +139,11 @@ async function addPinnedNote() {
|
|||
const note = await os.apiWithDialog('notes/show', {
|
||||
noteId: fromUrl ?? value,
|
||||
});
|
||||
pinnedNotes.value = [{
|
||||
id: note.id,
|
||||
}, ...pinnedNotes.value];
|
||||
pinnedNoteIds.value.unshift(note.id);
|
||||
}
|
||||
|
||||
function removePinnedNote(index: number) {
|
||||
pinnedNotes.value.splice(index, 1);
|
||||
function removePinnedNote(id: string) {
|
||||
pinnedNoteIds.value = pinnedNoteIds.value.filter(x => x !== id);
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
|
@ -166,7 +160,7 @@ function save() {
|
|||
os.apiWithDialog('channels/update', {
|
||||
...params,
|
||||
channelId: props.channelId,
|
||||
pinnedNoteIds: pinnedNotes.value.map(x => x.id),
|
||||
pinnedNoteIds: pinnedNoteIds.value,
|
||||
});
|
||||
} else {
|
||||
os.apiWithDialog('channels/create', params).then(created => {
|
||||
|
|
|
|||
|
|
@ -4,36 +4,41 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
-->
|
||||
|
||||
<template>
|
||||
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
|
||||
<template #item="{element}">
|
||||
<div :class="$style.item">
|
||||
<MkDraggable
|
||||
:modelValue="modelValue"
|
||||
direction="vertical"
|
||||
withGaps
|
||||
canNest
|
||||
group="pageBlocks"
|
||||
@update:modelValue="v => emit('update:modelValue', v)"
|
||||
>
|
||||
<template #default="{ item }">
|
||||
<div>
|
||||
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->
|
||||
<component :is="getComponent(element.type)" :modelValue="element" @update:modelValue="updateItem" @remove="() => removeItem(element)"/>
|
||||
<component :is="getComponent(item.type) as any" :modelValue="item" @update:modelValue="updateItem" @remove="() => removeItem(item)"/>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XSection from './els/page-editor.el.section.vue';
|
||||
import XText from './els/page-editor.el.text.vue';
|
||||
import XImage from './els/page-editor.el.image.vue';
|
||||
import XNote from './els/page-editor.el.note.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
|
||||
function getComponent(type: string) {
|
||||
function getComponent(type: Misskey.entities.Page['content'][number]['type']) {
|
||||
switch (type) {
|
||||
case 'section': return XSection;
|
||||
case 'text': return XText;
|
||||
case 'image': return XImage;
|
||||
case 'note': return XNote;
|
||||
default: return null;
|
||||
default: return XText;
|
||||
}
|
||||
}
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: Misskey.entities.Page['content'];
|
||||
}>();
|
||||
|
|
@ -61,11 +66,3 @@ function removeItem(el) {
|
|||
emit('update:modelValue', newValue);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.item {
|
||||
& + .item {
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -18,19 +18,18 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<div>
|
||||
<div v-panel style="border-radius: 6px;">
|
||||
<Sortable
|
||||
v-model="emojis"
|
||||
<MkDraggable
|
||||
:modelValue="emojis.map(emoji => ({ id: emoji, emoji }))"
|
||||
direction="horizontal"
|
||||
:class="$style.emojis"
|
||||
:itemKey="item => item"
|
||||
:animation="150"
|
||||
:delay="100"
|
||||
:delayOnTouchOnly="true"
|
||||
:group="{ name: 'SortableEmojiPalettes' }"
|
||||
group="emojiPalettes"
|
||||
@update:modelValue="v => emojis = v.map(x => x.emoji)"
|
||||
>
|
||||
<template #item="{element}">
|
||||
<button class="_button" :class="$style.emojisItem" @click="remove(element, $event)">
|
||||
<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
|
||||
<MkEmoji v-else :emoji="element" :normal="true"/>
|
||||
<template #default="{ item }">
|
||||
<button class="_button" :class="$style.emojisItem" @click="remove(item.emoji, $event)">
|
||||
<!-- pointer-eventsをnoneにしておかないとiOSなどでドラッグしたときに画像の方に判定が持ってかれる -->
|
||||
<MkCustomEmoji v-if="item.emoji[0] === ':'" style="pointer-events: none;" :name="item.emoji" :normal="true" :fallbackToImage="true"/>
|
||||
<MkEmoji v-else style="pointer-events: none;" :emoji="item.emoji" :normal="true"/>
|
||||
</button>
|
||||
</template>
|
||||
<template #footer>
|
||||
|
|
@ -38,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<i class="ti ti-plus"></i>
|
||||
</button>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
</div>
|
||||
<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
|
||||
</div>
|
||||
|
|
@ -47,7 +46,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch } from 'vue';
|
||||
import Sortable from 'vuedraggable';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -55,6 +53,7 @@ import { deepClone } from '@/utility/clone.js';
|
|||
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
|
||||
import MkEmoji from '@/components/global/MkEmoji.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
|
||||
|
||||
const props = defineProps<{
|
||||
|
|
|
|||
|
|
@ -9,25 +9,21 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<FormSlot>
|
||||
<template #label>{{ i18n.ts.navbar }}</template>
|
||||
<MkContainer :showHeader="false">
|
||||
<Sortable
|
||||
<MkDraggable
|
||||
v-model="items"
|
||||
itemKey="id"
|
||||
:animation="150"
|
||||
:handle="'.' + $style.itemHandle"
|
||||
@start="e => e.item.classList.add('active')"
|
||||
@end="e => e.item.classList.remove('active')"
|
||||
direction="vertical"
|
||||
>
|
||||
<template #item="{element,index}">
|
||||
<template #default="{ item }">
|
||||
<div
|
||||
v-if="element.type === '-' || navbarItemDef[element.type]"
|
||||
v-if="item.type === '-' || navbarItemDef[item.type]"
|
||||
:class="$style.item"
|
||||
>
|
||||
<button class="_button" :class="$style.itemHandle"><i class="ti ti-menu"></i></button>
|
||||
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[element.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[element.type]?.title ?? i18n.ts.divider }}</span>
|
||||
<button class="_button" :class="$style.itemRemove" @click="removeItem(index)"><i class="ti ti-x"></i></button>
|
||||
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item.type]?.icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item.type]?.title ?? i18n.ts.divider }}</span>
|
||||
<button class="_button" :class="$style.itemRemove" @click="removeItem(item.id)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
</MkContainer>
|
||||
</FormSlot>
|
||||
<div class="_buttons">
|
||||
|
|
@ -54,13 +50,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, defineAsyncComponent, ref, watch } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import MkContainer from '@/components/MkContainer.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { navbarItemDef } from '@/navbar.js';
|
||||
import { store } from '@/store.js';
|
||||
|
|
@ -70,8 +67,6 @@ import { prefer } from '@/preferences.js';
|
|||
import { getInitialPrefValue } from '@/preferences/manager.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const items = ref(prefer.s.menu.map(x => ({
|
||||
id: genId(),
|
||||
type: x,
|
||||
|
|
@ -98,8 +93,8 @@ async function addItem() {
|
|||
}];
|
||||
}
|
||||
|
||||
function removeItem(index: number) {
|
||||
items.value.splice(index, 1);
|
||||
function removeItem(itemId: string) {
|
||||
items.value = items.value.filter(i => i.id !== itemId);
|
||||
}
|
||||
|
||||
function save() {
|
||||
|
|
|
|||
|
|
@ -75,30 +75,27 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div :class="$style.metadataRoot" class="_gaps_s">
|
||||
<MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
|
||||
|
||||
<Sortable
|
||||
<MkDraggable
|
||||
v-model="fields"
|
||||
class="_gaps_s"
|
||||
itemKey="id"
|
||||
:animation="150"
|
||||
:handle="'.' + $style.dragItemHandle"
|
||||
@start="e => e.item.classList.add('active')"
|
||||
@end="e => e.item.classList.remove('active')"
|
||||
direction="vertical"
|
||||
withGaps
|
||||
manualDragStart
|
||||
>
|
||||
<template #item="{element, index}">
|
||||
<template #default="{ item, dragStart }">
|
||||
<div v-panel :class="$style.fieldDragItem">
|
||||
<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
|
||||
<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
|
||||
<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1" :draggable="true" @dragstart.stop="dragStart"><i class="ti ti-menu"></i></button>
|
||||
<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(item.id)"><i class="ti ti-x"></i></button>
|
||||
<div :class="$style.dragItemForm">
|
||||
<FormSplit :minWidth="200">
|
||||
<MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
|
||||
<MkInput v-model="item.name" small :placeholder="i18n.ts._profile.metadataLabel">
|
||||
</MkInput>
|
||||
<MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
|
||||
<MkInput v-model="item.value" small :placeholder="i18n.ts._profile.metadataContent">
|
||||
</MkInput>
|
||||
</FormSplit>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
</MkDraggable>
|
||||
</div>
|
||||
</MkFolder>
|
||||
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
|
||||
|
|
@ -165,7 +162,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch, defineAsyncComponent } from 'vue';
|
||||
import { computed, reactive, ref, watch } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
|
|
@ -174,6 +171,7 @@ import FormSplit from '@/components/form/split.vue';
|
|||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import FormSlot from '@/components/form/slot.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
import MkDraggable from '@/components/MkDraggable.vue';
|
||||
import { chooseDriveFile } from '@/utility/drive.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
|
@ -188,8 +186,6 @@ import { genId } from '@/utility/id.js';
|
|||
|
||||
const $i = ensureSignin();
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
const reactionAcceptance = store.model('reactionAcceptance');
|
||||
|
||||
function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
|
||||
|
|
@ -228,8 +224,8 @@ while (fields.value.length < 4) {
|
|||
addField();
|
||||
}
|
||||
|
||||
function deleteField(index: number) {
|
||||
fields.value.splice(index, 1);
|
||||
function deleteField(itemId: string) {
|
||||
fields.value = fields.value.filter(f => f.id !== itemId);
|
||||
}
|
||||
|
||||
function saveFields() {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XWidgets :edit="editMode" :widgets="widgets" @addWidget="addWidget" @removeWidget="removeWidget" @updateWidget="updateWidget" @updateWidgets="updateWidgets" @exit="editMode = false"/>
|
||||
|
||||
<button v-if="editMode" class="_textButton" style="font-size: 0.9em;" @click="editMode = false"><i class="ti ti-check"></i> {{ i18n.ts.editWidgetsExit }}</button>
|
||||
<button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
|
||||
<button v-else class="_textButton" data-cy-widget-edit :class="$style.edit" style="font-size: 0.9em; margin-top: 16px;" @click="editMode = true"><i class="ti ti-pencil"></i> {{ i18n.ts.editWidgets }}</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ export function getConfig(): UserConfig {
|
|||
_ENV_: JSON.stringify(process.env.NODE_ENV),
|
||||
_DEV_: process.env.NODE_ENV !== 'production',
|
||||
_PERF_PREFIX_: JSON.stringify('Misskey:'),
|
||||
__VUE_OPTIONS_API__: true,
|
||||
__VUE_OPTIONS_API__: false,
|
||||
__VUE_PROD_DEVTOOLS__: false,
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -29,11 +29,11 @@
|
|||
],
|
||||
"devDependencies": {
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/node": "24.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"chokidar": "5.0.0",
|
||||
"esbuild": "0.27.1",
|
||||
"esbuild": "0.27.2",
|
||||
"execa": "9.6.1",
|
||||
"nodemon": "3.1.11",
|
||||
"tsx": "4.21.0"
|
||||
|
|
|
|||
|
|
@ -11,14 +11,14 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.10.2",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/wawoff2": "1.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"harfbuzzjs": "0.4.13",
|
||||
"harfbuzzjs": "0.4.14",
|
||||
"tsx": "4.21.0",
|
||||
"wawoff2": "2.0.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,11 +25,11 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/matter-js": "0.20.2",
|
||||
"@types/node": "24.10.2",
|
||||
"@types/node": "24.10.4",
|
||||
"@types/seedrandom": "3.0.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"esbuild": "0.27.1",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"esbuild": "0.27.2",
|
||||
"execa": "9.6.1",
|
||||
"nodemon": "3.1.11"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@
|
|||
"generate": "tsx src/generator.ts && eslint ./built/**/*.ts --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@readme/openapi-parser": "5.2.1",
|
||||
"@types/node": "24.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"@readme/openapi-parser": "5.4.0",
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"openapi-types": "12.1.3",
|
||||
"openapi-typescript": "7.10.1",
|
||||
"ts-case-convert": "2.1.0",
|
||||
"tsx": "4.21.0",
|
||||
"eslint": "9.39.1"
|
||||
"eslint": "9.39.2"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"type": "module",
|
||||
"name": "misskey-js",
|
||||
"version": "2026.1.0-alpha.0",
|
||||
"version": "2026.1.0-alpha.2",
|
||||
"description": "Misskey SDK for JavaScript",
|
||||
"license": "MIT",
|
||||
"main": "./built/index.js",
|
||||
|
|
@ -38,16 +38,16 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "7.55.2",
|
||||
"@types/node": "24.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"@vitest/coverage-v8": "4.0.15",
|
||||
"esbuild": "0.27.1",
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"esbuild": "0.27.2",
|
||||
"execa": "9.6.1",
|
||||
"ncp": "2.0.0",
|
||||
"nodemon": "3.1.11",
|
||||
"tsd": "0.33.0",
|
||||
"vitest": "4.0.15",
|
||||
"vitest": "4.0.16",
|
||||
"vitest-websocket-mock": "0.5.0"
|
||||
},
|
||||
"files": [
|
||||
|
|
|
|||
|
|
@ -24,10 +24,10 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.10.2",
|
||||
"@typescript-eslint/eslint-plugin": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"esbuild": "0.27.1",
|
||||
"@types/node": "24.10.4",
|
||||
"@typescript-eslint/eslint-plugin": "8.50.1",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"esbuild": "0.27.2",
|
||||
"execa": "9.6.1",
|
||||
"nodemon": "3.1.11"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -10,12 +10,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"i18n": "workspace:*",
|
||||
"esbuild": "0.27.1",
|
||||
"esbuild": "0.27.2",
|
||||
"idb-keyval": "6.2.2",
|
||||
"misskey-js": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "8.49.0",
|
||||
"@typescript-eslint/parser": "8.50.1",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.74",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"nodemon": "3.1.11"
|
||||
|
|
|
|||
2731
pnpm-lock.yaml
2731
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
|
@ -7,7 +7,7 @@ import * as fs from 'node:fs/promises';
|
|||
import * as path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import * as yaml from 'js-yaml';
|
||||
import buildTarball from './tarball.mjs';
|
||||
import { buildTarball } from './tarball.mjs';
|
||||
|
||||
const configDir = fileURLToPath(new URL('../.config', import.meta.url));
|
||||
const configPath = process.env.MISSKEY_CONFIG_YML
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
const packageJsonPath = __dirname + '/../package.json'
|
||||
|
||||
function build() {
|
||||
|
|
@ -3,11 +3,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
import { execSync } from 'node:child_process';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
(async () => {
|
||||
fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/backend/src-js', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/backend/node_modules', { recursive: true, force: true });
|
||||
|
||||
fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true });
|
||||
|
|
@ -3,10 +3,13 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
(async () => {
|
||||
fs.rmSync(__dirname + '/../packages/backend/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/backend/src-js', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/frontend-shared/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/frontend/built', { recursive: true, force: true });
|
||||
fs.rmSync(__dirname + '/../packages/frontend-embed/built', { recursive: true, force: true });
|
||||
|
|
@ -19,7 +19,7 @@ const ignore = [
|
|||
// Exclude files you don't want to include in the tarball here
|
||||
];
|
||||
|
||||
export default async function build() {
|
||||
export async function buildTarball() {
|
||||
const mkdirPromise = mkdir(resolve(cwd, 'built', 'tarball'), { recursive: true });
|
||||
const pack = new Pack({ cwd, gzip: true });
|
||||
const patterns = await walk({ path: cwd, ignoreFiles: ['.gitignore'] });
|
||||
|
|
|
|||
Loading…
Reference in New Issue