From 0f3ea536a0a94da1df0357cf11ef3dbaf011e2c3 Mon Sep 17 00:00:00 2001 From: aXenDeveloper Date: Sat, 21 Sep 2024 17:16:30 +0200 Subject: [PATCH 1/2] refactor: Upload files using Editor --- apps/backend/schema.gql | 1 + apps/frontend/src/graphql/types.ts | 1 + apps/frontend/src/plugins/core/langs/en.json | 3 +- .../create_database.service.ts | 1 - .../authorization/authorization.dto.ts | 8 +- .../authorization/authorization.service.ts | 46 ++- .../src/core/editor/upload/upload.service.ts | 30 +- packages/backend/src/core/files/files.cron.ts | 2 +- .../src/core/files/helpers/upload/helpers.ts | 2 +- .../authorization/authorization.dto.ts | 2 +- .../authorization/authorization.service.ts | 2 +- .../frontend/src/components/editor/editor.tsx | 55 ++-- .../editor/extensions/extensions.tsx | 97 +++---- .../editor/extensions/files/files.ts | 269 ++++++++++++++---- .../files}/hooks/delete-mutation-api.ts | 0 .../files/hooks/use-files-extension-editor.ts | 181 ++++++++++++ .../use-upload-files-handler-editor.ts.ts | 190 ------------- .../components/editor/footer/files/button.tsx | 19 +- .../editor/footer/files/item/item.tsx | 99 +------ .../components/editor/footer/files/list.tsx | 4 +- .../src/components/editor/footer/footer.tsx | 12 +- .../editor/hooks/use-editor-state.ts | 18 +- .../components/editor/read-only/read-only.tsx | 5 +- .../src/components/form/fields/input.tsx | 2 + ...dmin__sessions__authorization.generated.ts | 8 +- .../admin/admin__sessions__authorization.gql | 6 + packages/frontend/src/graphql/types.ts | 1 + packages/frontend/src/helpers/format-bytes.ts | 7 +- .../core/sign/in/use-sign-in-admin-view.ts | 4 +- .../frontend/src/hooks/use-session-admin.ts | 11 +- packages/frontend/src/hooks/use-session.ts | 6 +- .../src/views/admin/layout/providers.tsx | 1 + .../core/settings/legal/revalidate-api.ts | 9 +- .../create-edit-form-groups-members-admin.tsx | 20 +- .../views/files/files-settings-view.tsx | 35 ++- 35 files changed, 675 insertions(+), 482 deletions(-) rename packages/frontend/src/components/editor/{footer/files/item => extensions/files}/hooks/delete-mutation-api.ts (100%) create mode 100644 packages/frontend/src/components/editor/extensions/files/hooks/use-files-extension-editor.ts delete mode 100644 packages/frontend/src/components/editor/extensions/files/hooks/use-upload-files-handler-editor.ts.ts diff --git a/apps/backend/schema.gql b/apps/backend/schema.gql index 54df77ddc..3056719a4 100644 --- a/apps/backend/schema.gql +++ b/apps/backend/schema.gql @@ -10,6 +10,7 @@ enum AllowTypeFilesEnum { } type AuthorizationAdminSessionsObj { + files: FilesAuthorizationCoreSessions! restart_server: Boolean! user: AuthorizationCurrentUserObj version: String! diff --git a/apps/frontend/src/graphql/types.ts b/apps/frontend/src/graphql/types.ts index b5b3123b2..5fc19b4be 100644 --- a/apps/frontend/src/graphql/types.ts +++ b/apps/frontend/src/graphql/types.ts @@ -26,6 +26,7 @@ export const AllowTypeFilesEnum = { export type AllowTypeFilesEnum = typeof AllowTypeFilesEnum[keyof typeof AllowTypeFilesEnum]; export type AuthorizationAdminSessionsObj = { __typename?: 'AuthorizationAdminSessionsObj'; + files: FilesAuthorizationCoreSessions; restart_server: Scalars['Boolean']['output']; user?: Maybe; version: Scalars['String']['output']; diff --git a/apps/frontend/src/plugins/core/langs/en.json b/apps/frontend/src/plugins/core/langs/en.json index 855d4cd77..51cd0755f 100644 --- a/apps/frontend/src/plugins/core/langs/en.json +++ b/apps/frontend/src/plugins/core/langs/en.json @@ -147,7 +147,7 @@ }, "max_storage_for_submit": { "title": "Maximum storage reached!", - "desc": "You can't upload more files then total {size} for one submit." + "desc": "You can't upload more files then total {size, plural, =0 {unlimited} other {#}} for one submit." } } }, @@ -294,6 +294,7 @@ "desc": "Manage your files.", "search": "Search files by name...", "temp_file": "Temporary File will be deleted soon", + "storage_usage": "{used} of {total} ({percent}%) storage used", "table": { "file_size": "File Size", "count_uses": "Count Uses", diff --git a/packages/backend/src/core/admin/install/create_database/create_database.service.ts b/packages/backend/src/core/admin/install/create_database/create_database.service.ts index d0b80cd5c..29b5132b0 100644 --- a/packages/backend/src/core/admin/install/create_database/create_database.service.ts +++ b/packages/backend/src/core/admin/install/create_database/create_database.service.ts @@ -60,7 +60,6 @@ export class CreateDatabaseAdminInstallService { protected: true, guest: true, files_allow_upload: false, - files_total_max_storage: -1, }) .returning(); diff --git a/packages/backend/src/core/admin/sessions/authorization/authorization.dto.ts b/packages/backend/src/core/admin/sessions/authorization/authorization.dto.ts index 841ba7047..c0e8b1491 100644 --- a/packages/backend/src/core/admin/sessions/authorization/authorization.dto.ts +++ b/packages/backend/src/core/admin/sessions/authorization/authorization.dto.ts @@ -1,8 +1,14 @@ -import { AuthorizationCurrentUserObj } from '@/core/sessions/authorization/authorization.dto'; +import { + AuthorizationCurrentUserObj, + FilesAuthorizationCoreSessions, +} from '@/core/sessions/authorization/authorization.dto'; import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() export class AuthorizationAdminSessionsObj { + @Field(() => FilesAuthorizationCoreSessions) + files: FilesAuthorizationCoreSessions; + @Field(() => Boolean) restart_server: boolean; diff --git a/packages/backend/src/core/admin/sessions/authorization/authorization.service.ts b/packages/backend/src/core/admin/sessions/authorization/authorization.service.ts index 3c6e9d90c..4eb144b95 100644 --- a/packages/backend/src/core/admin/sessions/authorization/authorization.service.ts +++ b/packages/backend/src/core/admin/sessions/authorization/authorization.service.ts @@ -1,9 +1,11 @@ +import { core_files } from '@/database/schema/files'; import { core_sessions_known_devices } from '@/database/schema/sessions'; import { currentUnixDate, getUserAgentData, getUserIp } from '@/functions'; import { AccessDeniedError, getConfigFile, GqlContext, + InternalServerError, NotFoundError, } from '@/index'; import { getUser } from '@/utils/database/helpers/get-user'; @@ -11,7 +13,7 @@ import { InternalDatabaseService } from '@/utils/database/internal_database.serv import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; -import { eq } from 'drizzle-orm'; +import { eq, sum } from 'drizzle-orm'; import * as fs from 'fs'; import { join } from 'path'; @@ -31,7 +33,27 @@ export class AuthorizationAdminSessionsService { async authorization( context: GqlContext, ): Promise { - const user = await this.initialAuthorization(context); + const currentUser = await this.initialAuthorization(context); + const user = await this.databaseService.db.query.core_users.findFirst({ + where: (table, { eq }) => eq(table.id, currentUser.id), + columns: { + id: true, + }, + with: { + group: { + columns: { + files_allow_upload: true, + files_max_storage_for_submit: true, + files_total_max_storage: true, + }, + }, + }, + }); + + if (!user) { + throw new InternalServerError(); + } + const config = getConfigFile(); const packageJSONPath = join(__dirname, '../../../../../../package.json'); @@ -42,10 +64,28 @@ export class AuthorizationAdminSessionsService { fs.readFileSync(packageJSONPath, 'utf8'), ); + const countStorageUsedDb = await this.databaseService.db + .select({ + space_used: sum(core_files.file_size), + }) + .from(core_files) + .where(eq(core_files.user_id, currentUser.id)); + const countStorageUsed = +(countStorageUsedDb[0].space_used ?? 0); + return { - user, + user: currentUser, version: packageJSON.version, restart_server: config.restart_server, + files: { + allow_upload: user.group.files_allow_upload, + max_storage_for_submit: user.group.files_max_storage_for_submit + ? user.group.files_max_storage_for_submit * 1024 + : user.group.files_max_storage_for_submit, + total_max_storage: user.group.files_total_max_storage + ? user.group.files_total_max_storage * 1024 + : user.group.files_total_max_storage, + space_used: countStorageUsed, + }, }; } diff --git a/packages/backend/src/core/editor/upload/upload.service.ts b/packages/backend/src/core/editor/upload/upload.service.ts index da4b035b6..369d59abe 100644 --- a/packages/backend/src/core/editor/upload/upload.service.ts +++ b/packages/backend/src/core/editor/upload/upload.service.ts @@ -21,7 +21,7 @@ import { ShowCoreFiles } from '../../files/show/show.dto'; import { UploadCoreEditorArgs } from './upload.dto'; interface GetFilesAfterUploadArgs extends UploadCoreEditorArgs { - maxUploadSizeKb: number; + maxUploadSizeBytes: number; } @Injectable() @@ -59,7 +59,7 @@ export class UploadCoreEditorService extends HelpersUploadCoreFilesService { private async getFilesAfterUpload({ file, folder, - maxUploadSizeKb, + maxUploadSizeBytes, plugin, }: GetFilesAfterUploadArgs): Promise { const acceptMimeType = this.getAcceptMineType(); @@ -70,7 +70,7 @@ export class UploadCoreEditorService extends HelpersUploadCoreFilesService { }); const args: Omit = { file, - maxUploadSizeBytes: maxUploadSizeKb * 1024, + maxUploadSizeBytes, plugin, folder, }; @@ -127,19 +127,27 @@ export class UploadCoreEditorService extends HelpersUploadCoreFilesService { : 0; const remainingStorage = - findGroup.files_total_max_storage - countStorageUsed; - - const maxUploadSizeKb = - remainingStorage < findGroup.files_max_storage_for_submit && - remainingStorage > 0 - ? remainingStorage - : findGroup.files_max_storage_for_submit; + findGroup.files_total_max_storage !== 0 + ? findGroup.files_total_max_storage * 1024 - countStorageUsed + : 0; + const maxStorage = (() => { + if (remainingStorage) { + return findGroup.files_max_storage_for_submit + ? Math.min( + findGroup.files_max_storage_for_submit * 1024, + remainingStorage, + ) + : remainingStorage; + } + + return findGroup.files_max_storage_for_submit * 1024 || -1; + })(); const uploadFile = await this.getFilesAfterUpload({ file, plugin, folder, - maxUploadSizeKb, + maxUploadSizeBytes: maxStorage, }); const security_key = this.acceptMimeTypeToFrontend.includes( diff --git a/packages/backend/src/core/files/files.cron.ts b/packages/backend/src/core/files/files.cron.ts index 285f6c047..2d145f221 100644 --- a/packages/backend/src/core/files/files.cron.ts +++ b/packages/backend/src/core/files/files.cron.ts @@ -19,7 +19,7 @@ export class CoreFilesCron { .select() .from(core_files) .leftJoin(core_files_using, eq(core_files_using.file_id, core_files.id)) - .where(lt(core_files.created, new Date(Date.now() - 1000 * 60 * 60 * 24))) + .where(lt(core_files.created, new Date(Date.now() - 1000 * 60 * 60))) // 1 hours .groupBy( core_files.id, core_files_using.file_id, diff --git a/packages/backend/src/core/files/helpers/upload/helpers.ts b/packages/backend/src/core/files/helpers/upload/helpers.ts index d6278dd7e..dfd5279c5 100644 --- a/packages/backend/src/core/files/helpers/upload/helpers.ts +++ b/packages/backend/src/core/files/helpers/upload/helpers.ts @@ -57,7 +57,7 @@ export class HelpersUploadCoreFilesService { const fileSizeInBytes = Buffer.concat(chunks).length; - if (fileSizeInBytes > maxUploadSizeBytes) { + if (fileSizeInBytes > maxUploadSizeBytes && maxUploadSizeBytes !== -1) { throw new CustomError({ code: 'FILE_TOO_LARGE', message: `${filename} file is too large! We only accept files up to ${maxUploadSizeBytes} bytes.`, diff --git a/packages/backend/src/core/sessions/authorization/authorization.dto.ts b/packages/backend/src/core/sessions/authorization/authorization.dto.ts index 2ddf7d847..a1614a143 100644 --- a/packages/backend/src/core/sessions/authorization/authorization.dto.ts +++ b/packages/backend/src/core/sessions/authorization/authorization.dto.ts @@ -17,7 +17,7 @@ export class AuthorizationCurrentUserObj extends User { } @ObjectType() -class FilesAuthorizationCoreSessions { +export class FilesAuthorizationCoreSessions { @Field(() => Boolean) allow_upload: boolean; diff --git a/packages/backend/src/core/sessions/authorization/authorization.service.ts b/packages/backend/src/core/sessions/authorization/authorization.service.ts index d82592c19..fb9f4cf97 100644 --- a/packages/backend/src/core/sessions/authorization/authorization.service.ts +++ b/packages/backend/src/core/sessions/authorization/authorization.service.ts @@ -110,7 +110,7 @@ export class AuthorizationCoreSessionsService { total_max_storage: user.group.files_total_max_storage ? user.group.files_total_max_storage * 1024 : user.group.files_total_max_storage, - space_used: countStorageUsed * 1024, + space_used: countStorageUsed, }, }; } catch (_) { diff --git a/packages/frontend/src/components/editor/editor.tsx b/packages/frontend/src/components/editor/editor.tsx index 29fb9e68e..203175335 100644 --- a/packages/frontend/src/components/editor/editor.tsx +++ b/packages/frontend/src/components/editor/editor.tsx @@ -1,6 +1,8 @@ 'use client'; import { StringLanguage } from '@/graphql/types'; +import { useSession } from '@/hooks/use-session'; +import { useSessionAdmin } from '@/hooks/use-session-admin'; import { Content, EditorContent, useEditor } from '@tiptap/react'; import { useLocale } from 'next-intl'; import React from 'react'; @@ -9,16 +11,18 @@ import { cn } from '../../helpers/classnames'; import { useGlobals } from '../../hooks/use-globals'; import { Skeleton } from '../ui/skeleton'; import { EmojiExtensionEditor } from './extensions/emoji/emoji'; -import { extensionsEditor } from './extensions/extensions'; -import { - UploadFilesHandlerEditorArgs, - useUploadFilesHandlerEditor, -} from './extensions/files/hooks/use-upload-files-handler-editor.ts'; +import { useExtensionsEditor } from './extensions/extensions'; +import { getFilesFromContent } from './extensions/files/hooks/functions'; +import { useFilesExtensionEditor } from './extensions/files/hooks/use-files-extension-editor'; import { FooterEditor } from './footer/footer'; import { EditorStateContext } from './hooks/use-editor-state'; import { ToolBarEditor } from './toolbar/toolbar'; -interface Props extends Omit { +interface Props { + allowUploadFiles?: { + folder: string; + plugin: string; + }; autofocus?: boolean; className?: string; disabled?: boolean; @@ -49,24 +53,36 @@ export const Editor = ({ value, disabled, }: WithLanguage | WithoutLanguage) => { - const { files, setFiles, uploadFiles } = useUploadFilesHandlerEditor({ - value, - allowUploadFiles, - }); const locale = useLocale(); const { defaultLanguage } = useGlobals(); const [selectedLanguage, setSelectedLanguage] = React.useState( locale || defaultLanguage, ); + const session = useSession(); + const adminSession = useSessionAdmin(); + const allowUploadFilesSession = + session.files.allow_upload || adminSession.files.allow_upload; + const { handleDelete, checkUploadFile, uploadFile } = useFilesExtensionEditor( + { + allowUploadFiles, + }, + ); + const extensions = useExtensionsEditor({ + fileSystem: { + editorValue: value, + files: Array.isArray(value) ? getFilesFromContent(value) : [], + selectedLanguage, + handleDelete, + checkUploadFile, + uploadFile, + allowUpload: allowUploadFilesSession, + }, + }); + const editor = useEditor({ autofocus: !!autofocus, immediatelyRender: false, - extensions: [ - ...extensionsEditor({ - uploadFiles, - }), - EmojiExtensionEditor, - ], + extensions: [...extensions, EmojiExtensionEditor], editorProps: { attributes: { class: cn( @@ -132,14 +148,13 @@ export const Editor = ({ return ( void, selectedLanguage, - setFiles, }} >
[ - StarterKit.configure({ - heading: false, - bulletList: { - HTMLAttributes: { - class: 'pl-5 list-disc', +export const useExtensionsEditor = ({ fileSystem }: FilesHandlerProps) => { + return [ + StarterKit.configure({ + heading: false, + bulletList: { + HTMLAttributes: { + class: 'pl-5 list-disc', + }, }, - }, - listItem: { - HTMLAttributes: { - class: 'ml-1 [&>p:first-of-type]:mb-0 [&:not(:first-child)]:mt-1', + listItem: { + HTMLAttributes: { + class: 'ml-1 [&>p:first-of-type]:mb-0 [&:not(:first-child)]:mt-1', + }, }, - }, - orderedList: { - HTMLAttributes: { - class: 'pl-5 list-decimal', + orderedList: { + HTMLAttributes: { + class: 'pl-5 list-decimal', + }, }, - }, - blockquote: { - HTMLAttributes: { - class: - 'border-l-[.25em] border-muted-foreground ml-4 [&:not(:last-child)]:mb-4 px-[1em] text-muted-foreground [&>p:nth-last-child(n)]:mb-0', + blockquote: { + HTMLAttributes: { + class: + 'border-l-[.25em] border-muted-foreground ml-4 [&:not(:last-child)]:mb-4 px-[1em] text-muted-foreground [&>p:nth-last-child(n)]:mb-0', + }, }, - }, - horizontalRule: { - HTMLAttributes: { - class: 'border-t border-muted-foreground/20 my-4', + horizontalRule: { + HTMLAttributes: { + class: 'border-t border-muted-foreground/20 my-4', + }, }, - }, - codeBlock: false, - code: { - HTMLAttributes: { - class: 'px-[.2em] py-[.4em] bg-muted-foreground/20 rounded-md', + codeBlock: false, + code: { + HTMLAttributes: { + class: 'px-[.2em] py-[.4em] bg-muted-foreground/20 rounded-md', + }, }, - }, - }), - Underline, - TextAlign.configure({ - types: ['heading', 'paragraph'], - }), - CodeBlockLowlightExtensionEditor, - Link.extend({ inclusive: false }).configure({ - openOnClick: true, - }), - Color, - TextStyle, - MentionExtensionEditor, - FilesHandler({ uploadFiles }), - HeadingExtensionEditor(), -]; + }), + Underline, + TextAlign.configure({ + types: ['heading', 'paragraph'], + }), + CodeBlockLowlightExtensionEditor, + Link.extend({ inclusive: false }).configure({ + openOnClick: true, + }), + Color, + TextStyle, + MentionExtensionEditor, + FilesHandler({ + fileSystem, + }), + HeadingExtensionEditor(), + ]; +}; diff --git a/packages/frontend/src/components/editor/extensions/files/files.ts b/packages/frontend/src/components/editor/extensions/files/files.ts index 39708c84c..b538d3172 100644 --- a/packages/frontend/src/components/editor/extensions/files/files.ts +++ b/packages/frontend/src/components/editor/extensions/files/files.ts @@ -1,9 +1,9 @@ import { Core_Editor_Files__UploadMutation } from '@/graphql/mutations/editor/core_editor_files__upload.generated'; +import { StringLanguage } from '@/graphql/types'; import { Plugin } from '@tiptap/pm/state'; -import { mergeAttributes, Node } from '@tiptap/react'; +import { JSONContent, mergeAttributes, Node } from '@tiptap/react'; import { renderReactNode } from './client'; -import { UploadFilesHandlerArgs } from './hooks/use-upload-files-handler-editor.ts'; export const acceptMimeTypeImage = [ 'image/jpeg', @@ -31,7 +31,9 @@ export interface FilesHandlerAttributes { declare module '@tiptap/react' { interface Commands { files: { - insertFile: (options: FilesHandlerAttributes) => ReturnType; + deleteFile: (id: number) => ReturnType; + insertFileIntoContent: (id: number) => ReturnType; + uploadFiles: (file: File[]) => ReturnType; }; } } @@ -44,12 +46,26 @@ export interface FileStateEditor { isLoading: boolean; } -export interface FilesHandlerArgs { - uploadFiles?: (args: UploadFilesHandlerArgs) => Promise; +export interface FilesHandlerProps { + fileSystem?: { + allowUpload: boolean; + checkUploadFile: (args: { + file: FileStateEditor; + fileState: FileStateEditor[]; + }) => FileStateEditor | undefined; + editorValue: string | StringLanguage[]; + files: FileStateEditor[]; + handleDelete: (args: { + id: number; + securityKey: string | undefined; + }) => Promise; + selectedLanguage: string; + uploadFile: (file: FileStateEditor) => Promise; + }; } -export const FilesHandler = ({ uploadFiles }: FilesHandlerArgs) => - Node.create({ +export const FilesHandler = ({ fileSystem }: FilesHandlerProps) => + Node.create({ name: 'files', group: 'inline', inline: true, @@ -59,6 +75,12 @@ export const FilesHandler = ({ uploadFiles }: FilesHandlerArgs) => isolating: false, priority: 10000, + addStorage() { + return { + files: fileSystem?.files ?? [], + }; + }, + addAttributes() { return { file_name_original: { @@ -109,72 +131,215 @@ export const FilesHandler = ({ uploadFiles }: FilesHandlerArgs) => }, addCommands() { + const handleDelete = ({ + content, + file_id, + }: { + content: string; + file_id: number; + }): string => { + const parseValue: { content: JSONContent[]; type: string } = + JSON.parse(content); + + const mapContent = (values: JSONContent[]): JSONContent[] => { + return values.filter(value => { + if (value.type === 'files' && value.attrs?.id === file_id) { + return false; + } + if (value.content) { + value.content = mapContent(value.content); + } + + return true; + }); + }; + + const valueReturn = { + ...parseValue, + content: mapContent(parseValue.content), + }; + + return JSON.stringify(valueReturn); + }; + return { - insertFile: - options => + insertFileIntoContent: + id => ({ commands }) => { + const files = this.storage.files.find(file => file.id === id); + + if (!files) return false; + return commands.insertContent({ type: this.name, - attrs: options, + attrs: files.data, }); }, + uploadFiles: files => () => { + if (!fileSystem?.editorValue || !files.length) return false; + const newFiles: FileStateEditor[] = files.map(file => ({ + file, + isLoading: true, + id: Math.floor(Math.random() * 1000) + file.size, + })); + this.storage.files = [...this.storage.files, ...newFiles]; + + void Promise.all( + newFiles + .map(async file => { + const findIndex = this.storage.files.findIndex( + item => item.id === file.id, + ); + if (findIndex === -1) return; + + const fileAfterProcess = fileSystem.checkUploadFile({ + file, + fileState: this.storage.files, + }); + if (!fileAfterProcess) return; + this.storage.files[findIndex] = fileAfterProcess; + if (fileAfterProcess.error) return; + + const fileAfterUpload = + await fileSystem.uploadFile(fileAfterProcess); + this.storage.files[findIndex] = fileAfterUpload; + + return fileAfterUpload; + }) + .filter(Boolean) as Promise[], + ); + + return true; + }, + deleteFile: + id => + ({ commands }) => { + if (!fileSystem?.editorValue) return false; + const file = this.storage.files.find(file => file.id === id); + if (!file) return false; + + if ( + Array.isArray(fileSystem.editorValue) && + fileSystem.editorValue.length > 0 + ) { + const content: StringLanguage[] = fileSystem.editorValue.map( + item => ({ + language_code: item.language_code, + value: handleDelete({ + content: item.value, + file_id: id, + }), + }), + ); + + const parseContent: string = JSON.parse( + content.find( + item => item.language_code === fileSystem.selectedLanguage, + )?.value ?? '', + ); + + commands.clearContent(); + commands.setContent(parseContent); + } else if (typeof fileSystem.editorValue === 'string') { + const content = handleDelete({ + content: fileSystem.editorValue, + file_id: id, + }); + + commands.clearContent(); + commands.setContent(content); + } + + this.storage.files = this.storage.files.filter( + file => file.id !== id, + ); + if (file.data) { + void fileSystem.handleDelete({ + id, + securityKey: file.data.security_key, + }); + } + + return true; + }, }; }, addProseMirrorPlugins() { + const handleUploadFiles = async ( + files: File[], + finishUploadCallback?: (file: FileStateEditor) => void, + ): Promise => { + if (!files.length || !fileSystem?.allowUpload) return []; + const newFiles: FileStateEditor[] = files.map(file => ({ + file, + isLoading: true, + id: Math.floor(Math.random() * 1000) + file.size, + })); + + this.storage.files = [...this.storage.files, ...newFiles]; + + return ( + await Promise.all( + newFiles.map(async file => { + const findIndex = this.storage.files.findIndex( + item => item.id === file.id, + ); + if (findIndex === -1) return; + + const fileAfterProcess = fileSystem.checkUploadFile({ + file, + fileState: this.storage.files, + }); + if (!fileAfterProcess) return; + this.storage.files[findIndex] = fileAfterProcess; + if (fileAfterProcess.error) return fileAfterProcess; + + const fileAfterUpload = + await fileSystem.uploadFile(fileAfterProcess); + this.storage.files[findIndex] = fileAfterUpload; + + finishUploadCallback?.(fileAfterUpload); + + return fileAfterUpload; + }), + ) + ).filter(Boolean) as FileStateEditor[]; + }; + return [ new Plugin({ props: { handlePaste(view, event) { - const files: FileStateEditor[] = [ - ...(event.clipboardData?.files ?? []), - ].map(file => ({ - file, - isLoading: true, - id: Math.floor(Math.random() * 1000) + file.size, - })); - if (!files.length || !uploadFiles) return false; + const files = [...(event.clipboardData?.files ?? [])]; + if (!files.length) return false; const { schema } = view.state; - void uploadFiles({ - files, - finishUpload: file => { - const node = schema.nodes.files.create(file.data); - const transaction = view.state.tr.replaceSelectionWith(node); - view.dispatch(transaction); - }, + void handleUploadFiles(files, file => { + const node = schema.nodes.files.create(file.data); + const transaction = view.state.tr.replaceSelectionWith(node); + view.dispatch(transaction); }); return true; }, + handleDrop(view, event, slice, moved) { - const files: FileStateEditor[] = [ - ...(event.dataTransfer?.files ?? []), - ].map(file => ({ - file, - isLoading: true, - id: Math.floor(Math.random() * 1000) + file.size, - })); - if ((moved && !files.length) || !uploadFiles) return false; - - void uploadFiles({ - files, - finishUpload: file => { - const { schema } = view.state; - const coordinates = view.posAtCoords({ - left: event.clientX, - top: event.clientY, - }); - - if (!coordinates) return; - - const node = schema.nodes.files.create(file.data); - const transaction = view.state.tr.insert( - coordinates.pos, - node, - ); - view.dispatch(transaction); - }, + const files = [...(event.dataTransfer?.files ?? [])]; + if (moved && !files.length) return false; + + void handleUploadFiles(files, file => { + const { schema } = view.state; + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (!coordinates) return; + + const node = schema.nodes.files.create(file.data); + const transaction = view.state.tr.insert(coordinates.pos, node); + view.dispatch(transaction); }); return true; diff --git a/packages/frontend/src/components/editor/footer/files/item/hooks/delete-mutation-api.ts b/packages/frontend/src/components/editor/extensions/files/hooks/delete-mutation-api.ts similarity index 100% rename from packages/frontend/src/components/editor/footer/files/item/hooks/delete-mutation-api.ts rename to packages/frontend/src/components/editor/extensions/files/hooks/delete-mutation-api.ts diff --git a/packages/frontend/src/components/editor/extensions/files/hooks/use-files-extension-editor.ts b/packages/frontend/src/components/editor/extensions/files/hooks/use-files-extension-editor.ts new file mode 100644 index 000000000..b1f647fbc --- /dev/null +++ b/packages/frontend/src/components/editor/extensions/files/hooks/use-files-extension-editor.ts @@ -0,0 +1,181 @@ +import { FilesAuthorizationCoreSessions } from '@/graphql/types'; +import { useGlobals } from '@/hooks/use-globals'; +import { useSession } from '@/hooks/use-session'; +import { useSessionAdmin } from '@/hooks/use-session-admin'; +import { useTranslations } from 'next-intl'; +import { toast } from 'sonner'; + +import { + acceptMimeTypeImage, + acceptMimeTypeVideo, + FileStateEditor, +} from '../files'; +import { deleteMutationApi } from './delete-mutation-api'; +import { uploadMutationApi } from './upload-mutation-api'; + +export const useFilesExtensionEditor = ({ + allowUploadFiles, +}: { + allowUploadFiles?: { + folder: string; + plugin: string; + }; +}) => { + const tCore = useTranslations('core.errors'); + const session = useSession(); + const adminSession = useSessionAdmin(); + const { config } = useGlobals(); + const permissionFiles: FilesAuthorizationCoreSessions = { + allow_upload: session.files.allow_upload || adminSession.files.allow_upload, + max_storage_for_submit: + session.files.max_storage_for_submit || + adminSession.files.max_storage_for_submit, + space_used: session.files.space_used || adminSession.files.space_used, + total_max_storage: + session.files.total_max_storage || adminSession.files.total_max_storage, + }; + + const handleDelete = async ({ + id, + securityKey, + }: { + id: number; + securityKey: string | undefined; + }) => { + const mutation = await deleteMutationApi({ + id, + securityKey, + }); + + if (mutation?.error) { + toast.error(tCore('title'), { + description: tCore('internal_server_error'), + }); + } + }; + + const validateMimeTypeFile = (file: FileStateEditor): FileStateEditor => { + if (file.error) + return { ...file, error: 'Internal Server Error', isLoading: false }; + + const { allow_type } = config.editor.files; + + if (allow_type === 'all') return file; + + const isValidType = (types: string[]) => + types.includes(file.file?.type ?? ''); + + if (allow_type === 'images_videos') { + if (!isValidType([...acceptMimeTypeImage, ...acceptMimeTypeVideo])) { + return { ...file, error: 'Invalid file type', isLoading: false }; + } + } else if (allow_type === 'images') { + if (!isValidType(acceptMimeTypeImage)) { + return { ...file, error: 'Invalid file type', isLoading: false }; + } + } + + return file; + }; + + const validateSizeFile = ({ + file, + fileState, + }: { + file: FileStateEditor; + fileState: FileStateEditor[]; + }): FileStateEditor => { + if (file.error) + return { ...file, error: 'Internal Server Error', isLoading: false }; + + if ( + permissionFiles.max_storage_for_submit === 0 && + permissionFiles.total_max_storage === 0 + ) { + return file; + } + + const remainingStorage = + permissionFiles.total_max_storage !== 0 + ? permissionFiles.total_max_storage - permissionFiles.space_used + : 0; + + const maxStorage = (() => { + if (remainingStorage) { + return permissionFiles.max_storage_for_submit + ? Math.min(permissionFiles.max_storage_for_submit, remainingStorage) + : remainingStorage; + } + + return permissionFiles.max_storage_for_submit || -1; + })(); + const totalSize = [file, ...fileState.filter(i => i.id !== file.id)].reduce( + (acc, file) => { + if (file.data) return acc + file.data.file_size; + if (file.file) return acc + file.file.size; + + return acc; + }, + 0, + ); + + if (totalSize > maxStorage && maxStorage !== -1) { + return { ...file, error: 'Max storage exceeded', isLoading: false }; + } + + return file; + }; + + const checkUploadFile = ({ + file, + fileState, + }: { + file: FileStateEditor; + fileState: FileStateEditor[]; + }) => { + if ( + !allowUploadFiles || + config.editor.files.allow_type === 'none' || + !permissionFiles.allow_upload + ) { + return; + } + + const fileAfterCheckMineType = validateMimeTypeFile(file); + if (fileAfterCheckMineType.error) return fileAfterCheckMineType; + const fileAfterCheckSize = validateSizeFile({ + file: fileAfterCheckMineType, + fileState, + }); + if (fileAfterCheckSize.error) return fileAfterCheckSize; + + return file; + }; + + const uploadFile = async ( + file: FileStateEditor, + ): Promise => { + const formData = new FormData(); + if (!file.file || !allowUploadFiles) { + return { ...file, error: 'Internal Server Error', isLoading: false }; + } + formData.append('file', file.file); + formData.append('plugin', allowUploadFiles.plugin); + formData.append('folder', allowUploadFiles.folder); + const mutation = await uploadMutationApi(formData); + + if (mutation.error || !mutation.data?.core_editor_files__upload) { + return { ...file, error: 'Internal Server Error', isLoading: false }; + } + const { core_editor_files__upload } = mutation.data; + + return { + data: core_editor_files__upload, + id: core_editor_files__upload.id, + isLoading: false, + error: '', + }; + }; + + return { handleDelete, checkUploadFile, uploadFile }; +}; diff --git a/packages/frontend/src/components/editor/extensions/files/hooks/use-upload-files-handler-editor.ts.ts b/packages/frontend/src/components/editor/extensions/files/hooks/use-upload-files-handler-editor.ts.ts deleted file mode 100644 index dd3368bbb..000000000 --- a/packages/frontend/src/components/editor/extensions/files/hooks/use-upload-files-handler-editor.ts.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { StringLanguage } from '@/graphql/types'; -import { formatBytes } from '@/helpers/format-bytes'; -import { useTranslations } from 'next-intl'; -import React from 'react'; -import { toast } from 'sonner'; -import { useGlobals } from 'vitnode-frontend/hooks/use-globals'; -import { useSession } from 'vitnode-frontend/hooks/use-session'; - -import { - acceptMimeTypeImage, - acceptMimeTypeVideo, - FileStateEditor, -} from '../files'; -import { getFilesFromContent } from './functions'; -import { uploadMutationApi } from './upload-mutation-api'; - -export interface UploadFilesHandlerArgs { - files: FileStateEditor[]; - finishUpload?: (file: FileStateEditor) => void; -} - -export interface UploadFilesHandlerEditorArgs { - allowUploadFiles?: { - folder: string; - plugin: string; - }; - value: string | StringLanguage[]; -} - -export const useUploadFilesHandlerEditor = ({ - allowUploadFiles, - value, -}: UploadFilesHandlerEditorArgs) => { - const { files: permissionFiles } = useSession(); - const { config } = useGlobals(); - const [files, setFiles] = React.useState( - Array.isArray(value) ? getFilesFromContent(value) : [], - ); - const t = useTranslations('core.editor.files'); - const tCore = useTranslations('core'); - - const handleUpload = async ({ - data, - finishUpload, - }: { - data: FileStateEditor; - finishUpload?: (file: FileStateEditor) => void; - }) => { - const formData = new FormData(); - if (!data.file || !allowUploadFiles) return; - formData.append('file', data.file); - formData.append('plugin', allowUploadFiles.plugin); - formData.append('folder', allowUploadFiles.folder); - const mutation = await uploadMutationApi(formData); - - if (!mutation.data) { - toast.error(tCore('errors.title'), { - description: tCore('errors.internal_server_error'), - }); - - setFiles(prev => - prev.map(item => { - if (item.id === data.id) { - return { - ...item, - error: tCore('errors.internal_server_error'), - isLoading: false, - }; - } - - return item; - }), - ); - - return; - } - - setFiles(prev => - prev.map(item => { - if (item.id === data.id) { - return { - ...item, - data: mutation.data.core_editor_files__upload, - isLoading: false, - id: mutation.data.core_editor_files__upload.id, - }; - } - - return item; - }), - ); - - finishUpload?.({ - ...data, - data: mutation.data.core_editor_files__upload, - id: mutation.data.core_editor_files__upload.id, - isLoading: false, - }); - }; - - const validateMineTypeFiles = ( - files: FileStateEditor[], - ): FileStateEditor[] => { - // console.log(files); - if (config.editor.files.allow_type === 'all') return files; - - return files - .filter(file => !file.error) - .filter(file => { - if (config.editor.files.allow_type === 'images_videos') { - return [...acceptMimeTypeImage, ...acceptMimeTypeVideo].includes( - file.file?.type ?? '', - ); - } - - if (config.editor.files.allow_type === 'images') { - return acceptMimeTypeImage.includes(file.file?.type ?? ''); - } - }); - }; - - const validateSizeFiles = (items: FileStateEditor[]): FileStateEditor[] => { - const remainingStorage = - permissionFiles.total_max_storage - permissionFiles.space_used; - const max = - remainingStorage < permissionFiles.max_storage_for_submit && - remainingStorage > 0 - ? remainingStorage - : permissionFiles.max_storage_for_submit; - const totalSize = [...files.filter(file => !file.error), ...items].reduce( - (acc, file) => { - if (!file.file) return acc; - - return acc + file.file.size; - }, - 0, - ); - - if (totalSize > max) { - toast.error(t('errors.max_storage_for_submit.title'), { - description: t.rich('errors.max_storage_for_submit.desc', { - size: formatBytes(max), - }), - }); - - return []; - } - - return items; - }; - - const uploadFiles = async ({ - files, - finishUpload, - }: UploadFilesHandlerArgs) => { - if ( - !files.length || - !allowUploadFiles || - config.editor.files.allow_type === 'none' || - !permissionFiles.allow_upload - ) { - return; - } - - const validateMineType = validateMineTypeFiles(files); - - if (validateMineType.length !== files.length) { - toast.error(t('errors.invalid_file_type.title'), { - description: t('errors.invalid_file_type.desc', { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-expect-error - types: t(config.editor.files.allow_type), - }), - }); - } - if (!validateMineType.length) return; - - const validateSize = validateSizeFiles(validateMineType); - if (!validateSize.length) return; - - setFiles(prev => [...prev, ...validateMineType]); - await Promise.all( - validateMineType.map(async data => { - await handleUpload({ data, finishUpload }); - }), - ); - }; - - return { uploadFiles, files, setFiles }; -}; diff --git a/packages/frontend/src/components/editor/footer/files/button.tsx b/packages/frontend/src/components/editor/footer/files/button.tsx index 000cf3690..dc981cce9 100644 --- a/packages/frontend/src/components/editor/footer/files/button.tsx +++ b/packages/frontend/src/components/editor/footer/files/button.tsx @@ -3,32 +3,23 @@ import { useTranslations } from 'next-intl'; import React from 'react'; import { Button } from '../../../ui/button'; -import { FileStateEditor } from '../../extensions/files/files'; import { useEditorState } from '../../hooks/use-editor-state'; export const FilesButtonFooterEditor = () => { - const t = useTranslations('core.editor'); + const t = useTranslations('core.editor.files'); const ref = React.useRef(null); - const { uploadFiles } = useEditorState(); + const { editor } = useEditorState(); return ( <> { - const files: FileStateEditor[] = [...(e.target.files ?? [])].map( - file => ({ - file, - isLoading: true, - id: Math.floor(Math.random() * 1000) + file.size, - }), - ); - - await uploadFiles({ files }); + onChange={e => { + editor.commands.uploadFiles([...(e.target.files ?? [])]); }} ref={ref} type="file" diff --git a/packages/frontend/src/components/editor/footer/files/item/item.tsx b/packages/frontend/src/components/editor/footer/files/item/item.tsx index 0bbf1c999..042c2a60e 100644 --- a/packages/frontend/src/components/editor/footer/files/item/item.tsx +++ b/packages/frontend/src/components/editor/footer/files/item/item.tsx @@ -1,16 +1,12 @@ import { Button } from '@/components/ui/button'; -import { StringLanguage } from '@/graphql/types'; import { cn } from '@/helpers/classnames'; import { CONFIG } from '@/helpers/config-with-env'; -import { JSONContent } from '@tiptap/react'; import { Plus, Trash2 } from 'lucide-react'; import { useTranslations } from 'next-intl'; -import { toast } from 'sonner'; import { FileStateEditor } from '../../../extensions/files/files'; import { useEditorState } from '../../../hooks/use-editor-state'; import { ContentItemListFilesFooterEditor } from './content'; -import { deleteMutationApi } from './hooks/delete-mutation-api'; import { IconItemListFilesFooterEditor } from './icon'; export interface ItemListFilesFooterEditorProps @@ -27,39 +23,7 @@ export const ItemListFilesFooterEditor = ({ }: ItemListFilesFooterEditorProps) => { const t = useTranslations('core.editor.files'); const tCore = useTranslations('core'); - const { editor, onChange, selectedLanguage, setFiles, value } = - useEditorState(); - - const handleDelete = ({ - content, - file_id, - }: { - content: string; - file_id: number; - }): string => { - const parseValue: { content: JSONContent[]; type: string } = - JSON.parse(content); - - const mapContent = (values: JSONContent[]): JSONContent[] => { - return values.filter(value => { - if (value.type === 'files' && value.attrs?.id === file_id) { - return false; - } - if (value.content) { - value.content = mapContent(value.content); - } - - return true; - }); - }; - - const valueReturn = { - ...parseValue, - content: mapContent(parseValue.content), - }; - - return JSON.stringify(valueReturn); - }; + const { editor } = useEditorState(); return ( <> @@ -84,9 +48,11 @@ export const ItemListFilesFooterEditor = ({
- - {file?.name ?? data?.file_name ?? 'Error!'} - + {!isLoading && ( + + {file?.name ?? data?.file_name ?? error} + + )}
{ - editor.commands.insertFile({ - ...data, - file_alt: data.file_alt ?? '', - width: data.width ?? 0, - height: data.height ?? 0, - security_key: data.security_key ?? '', - id, - }); + editor.commands.insertFileIntoContent(id); editor.commands.focus(); }} variant="ghost" @@ -120,48 +79,8 @@ export const ItemListFilesFooterEditor = ({ )}
{files.length > 0 && } diff --git a/packages/frontend/src/components/editor/hooks/use-editor-state.ts b/packages/frontend/src/components/editor/hooks/use-editor-state.ts index 6e270b1e4..1cb24d95c 100644 --- a/packages/frontend/src/components/editor/hooks/use-editor-state.ts +++ b/packages/frontend/src/components/editor/hooks/use-editor-state.ts @@ -2,30 +2,22 @@ import { StringLanguage } from '@/graphql/types'; import { Editor } from '@tiptap/react'; import React from 'react'; -import { FileStateEditor } from '../extensions/files/files'; -import { - UploadFilesHandlerArgs, - UploadFilesHandlerEditorArgs, -} from '../extensions/files/hooks/use-upload-files-handler-editor.ts'; - -interface Args extends Omit { +interface Args { + allowUploadFiles?: { + folder: string; + plugin: string; + }; editor: Editor; - files: FileStateEditor[]; onChange: (value: string | StringLanguage[]) => void; selectedLanguage: string; - setFiles: React.Dispatch>; - uploadFiles: (args: UploadFilesHandlerArgs) => Promise; value: string | StringLanguage[]; } export const EditorStateContext = React.createContext({ - files: [], editor: {} as Editor, - uploadFiles: async () => {}, value: [], onChange: () => {}, selectedLanguage: '', - setFiles: () => {}, }); export const useEditorState = () => React.useContext(EditorStateContext); diff --git a/packages/frontend/src/components/editor/read-only/read-only.tsx b/packages/frontend/src/components/editor/read-only/read-only.tsx index fcd46a295..7e53641f6 100644 --- a/packages/frontend/src/components/editor/read-only/read-only.tsx +++ b/packages/frontend/src/components/editor/read-only/read-only.tsx @@ -6,7 +6,7 @@ import Image from 'next/image'; import { useLocale } from 'next-intl'; import { cn } from '../../../helpers/classnames'; -import { extensionsEditor } from '../extensions/extensions'; +import { useExtensionsEditor } from '../extensions/extensions'; import { changeCodeBlock } from './code-block'; import { FileDownloadButton } from './file-download-button'; @@ -20,6 +20,7 @@ export const ReadOnlyEditor = ({ value: StringLanguage[]; }) => { const locale = useLocale(); + const extensions = useExtensionsEditor({}); const currentValue = (): string => { const current = @@ -48,7 +49,7 @@ export const ReadOnlyEditor = ({ try { const json: JSONContent = JSON.parse(currentValue()); - return generateHTML(json, extensionsEditor({})); + return generateHTML(json, extensions); } catch (_) { return currentValue(); } diff --git a/packages/frontend/src/components/form/fields/input.tsx b/packages/frontend/src/components/form/fields/input.tsx index 1fc231ef9..834b13063 100644 --- a/packages/frontend/src/components/form/fields/input.tsx +++ b/packages/frontend/src/components/form/fields/input.tsx @@ -50,6 +50,8 @@ export function AutoFormInput({ {...zodInputProps} {...componentProps} disabled={isDisabled || componentProps?.disabled} + onChange={field.onChange} + value={field.value} /> {ChildComponent && } diff --git a/packages/frontend/src/graphql/queries/admin/admin__sessions__authorization.generated.ts b/packages/frontend/src/graphql/queries/admin/admin__sessions__authorization.generated.ts index 923755bbc..d3c815427 100644 --- a/packages/frontend/src/graphql/queries/admin/admin__sessions__authorization.generated.ts +++ b/packages/frontend/src/graphql/queries/admin/admin__sessions__authorization.generated.ts @@ -4,7 +4,7 @@ import gql from 'graphql-tag'; export type Admin__Sessions__AuthorizationQueryVariables = Types.Exact<{ [key: string]: never; }>; -export type Admin__Sessions__AuthorizationQuery = { __typename?: 'Query', admin__sessions__authorization: { __typename?: 'AuthorizationAdminSessionsObj', version: string, restart_server: boolean, user?: { __typename?: 'AuthorizationCurrentUserObj', email: string, id: number, name_seo: string, is_admin: boolean, is_mod: boolean, name: string, newsletter: boolean, avatar_color: string, language: string, avatar?: { __typename?: 'AvatarUser', id: number, dir_folder: string, file_name: string }, group: { __typename?: 'GroupUser', color?: string, id: number, name: Array<{ __typename?: 'StringLanguage', language_code: string, value: string }> } } }, admin__nav__show: Array<{ __typename?: 'ShowAdminNavObj', code: string, nav: Array<{ __typename?: 'ShowAdminNavPluginsObj', code: string, icon?: string, children?: Array<{ __typename?: 'ShowAdminNavPlugins', icon?: string, code: string }> }> }> }; +export type Admin__Sessions__AuthorizationQuery = { __typename?: 'Query', admin__sessions__authorization: { __typename?: 'AuthorizationAdminSessionsObj', version: string, restart_server: boolean, user?: { __typename?: 'AuthorizationCurrentUserObj', email: string, id: number, name_seo: string, is_admin: boolean, is_mod: boolean, name: string, newsletter: boolean, avatar_color: string, language: string, avatar?: { __typename?: 'AvatarUser', id: number, dir_folder: string, file_name: string }, group: { __typename?: 'GroupUser', color?: string, id: number, name: Array<{ __typename?: 'StringLanguage', language_code: string, value: string }> } }, files: { __typename?: 'FilesAuthorizationCoreSessions', allow_upload: boolean, max_storage_for_submit: number, space_used: number, total_max_storage: number } }, admin__nav__show: Array<{ __typename?: 'ShowAdminNavObj', code: string, nav: Array<{ __typename?: 'ShowAdminNavPluginsObj', code: string, icon?: string, children?: Array<{ __typename?: 'ShowAdminNavPlugins', icon?: string, code: string }> }> }> }; export const Admin__Sessions__Authorization = gql` @@ -36,6 +36,12 @@ export const Admin__Sessions__Authorization = gql` } version restart_server + files { + allow_upload + max_storage_for_submit + space_used + total_max_storage + } } admin__nav__show { code diff --git a/packages/frontend/src/graphql/queries/admin/admin__sessions__authorization.gql b/packages/frontend/src/graphql/queries/admin/admin__sessions__authorization.gql index f3e370142..25f8dad15 100644 --- a/packages/frontend/src/graphql/queries/admin/admin__sessions__authorization.gql +++ b/packages/frontend/src/graphql/queries/admin/admin__sessions__authorization.gql @@ -26,6 +26,12 @@ query Admin__sessions__authorization { } version restart_server + files { + allow_upload + max_storage_for_submit + space_used + total_max_storage + } } admin__nav__show { code diff --git a/packages/frontend/src/graphql/types.ts b/packages/frontend/src/graphql/types.ts index b5b3123b2..5fc19b4be 100644 --- a/packages/frontend/src/graphql/types.ts +++ b/packages/frontend/src/graphql/types.ts @@ -26,6 +26,7 @@ export const AllowTypeFilesEnum = { export type AllowTypeFilesEnum = typeof AllowTypeFilesEnum[keyof typeof AllowTypeFilesEnum]; export type AuthorizationAdminSessionsObj = { __typename?: 'AuthorizationAdminSessionsObj'; + files: FilesAuthorizationCoreSessions; restart_server: Scalars['Boolean']['output']; user?: Maybe; version: Scalars['String']['output']; diff --git a/packages/frontend/src/helpers/format-bytes.ts b/packages/frontend/src/helpers/format-bytes.ts index 10cdfc4cf..865aa4edb 100644 --- a/packages/frontend/src/helpers/format-bytes.ts +++ b/packages/frontend/src/helpers/format-bytes.ts @@ -1,10 +1,11 @@ -// From: https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript export const formatBytes = (bytes: number, decimals = 2): string => { + if (bytes === 0) return '0 KB'; + const k = 1024; const dm = decimals < 0 ? 0 : decimals; - const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + const sizes = ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; + return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i - 1]}`; }; diff --git a/packages/frontend/src/hooks/core/sign/in/use-sign-in-admin-view.ts b/packages/frontend/src/hooks/core/sign/in/use-sign-in-admin-view.ts index 461ff77cf..7c8dde285 100644 --- a/packages/frontend/src/hooks/core/sign/in/use-sign-in-admin-view.ts +++ b/packages/frontend/src/hooks/core/sign/in/use-sign-in-admin-view.ts @@ -7,8 +7,8 @@ export const useSignInAdminView = () => { const [error, setError] = React.useState(''); const formSchema = z.object({ - email: z.string().min(1), - password: z.string().min(1), + email: z.string().min(1).default(''), + password: z.string().min(1).default(''), }); const onSubmit = async (values: z.infer) => { diff --git a/packages/frontend/src/hooks/use-session-admin.ts b/packages/frontend/src/hooks/use-session-admin.ts index c8b7fb7a7..1e6ce9d71 100644 --- a/packages/frontend/src/hooks/use-session-admin.ts +++ b/packages/frontend/src/hooks/use-session-admin.ts @@ -1,14 +1,21 @@ -import { AuthorizationCurrentUserObj } from '@/graphql/types'; +import { Admin__Sessions__AuthorizationQuery } from '@/graphql/queries/admin/admin__sessions__authorization.generated'; import React from 'react'; interface Args { - session: AuthorizationCurrentUserObj | undefined; + files: Admin__Sessions__AuthorizationQuery['admin__sessions__authorization']['files']; + session: Admin__Sessions__AuthorizationQuery['admin__sessions__authorization']['user']; version: string; } export const SessionAdminContext = React.createContext({ session: undefined, version: '', + files: { + allow_upload: false, + max_storage_for_submit: 0, + total_max_storage: 0, + space_used: 0, + }, }); export const useSessionAdmin = () => React.useContext(SessionAdminContext); diff --git a/packages/frontend/src/hooks/use-session.ts b/packages/frontend/src/hooks/use-session.ts index a53c1a8fa..4bb2eb6ff 100644 --- a/packages/frontend/src/hooks/use-session.ts +++ b/packages/frontend/src/hooks/use-session.ts @@ -12,9 +12,9 @@ export const SessionContext = React.createContext({ session: null, nav: [], files: { - allow_upload: true, - max_storage_for_submit: 10000, - total_max_storage: 500000, + allow_upload: false, + max_storage_for_submit: 0, + total_max_storage: 0, space_used: 0, }, }); diff --git a/packages/frontend/src/views/admin/layout/providers.tsx b/packages/frontend/src/views/admin/layout/providers.tsx index 4c2a6c530..9d47e3181 100644 --- a/packages/frontend/src/views/admin/layout/providers.tsx +++ b/packages/frontend/src/views/admin/layout/providers.tsx @@ -16,6 +16,7 @@ export const AdminProviders = ({ value={{ session: data.admin__sessions__authorization.user, version: data.admin__sessions__authorization.version, + files: data.admin__sessions__authorization.files, }} > {children} diff --git a/packages/frontend/src/views/admin/views/core/settings/legal/revalidate-api.ts b/packages/frontend/src/views/admin/views/core/settings/legal/revalidate-api.ts index b636256e4..73d8542ff 100644 --- a/packages/frontend/src/views/admin/views/core/settings/legal/revalidate-api.ts +++ b/packages/frontend/src/views/admin/views/core/settings/legal/revalidate-api.ts @@ -2,7 +2,12 @@ import { revalidateTag } from 'next/cache'; export const revalidateApi = (code?: string, prevCode?: string) => { revalidateTag('core_terms__show'); - if (code ?? prevCode) { - revalidateTag(`core_terms__show_${code ?? prevCode}`); + + if (code) { + revalidateTag(`core_terms__show-${code}`); + } + + if (prevCode) { + revalidateTag(`core_terms__show-${prevCode}`); } }; diff --git a/packages/frontend/src/views/admin/views/members/groups/create-edit-form/create-edit-form-groups-members-admin.tsx b/packages/frontend/src/views/admin/views/members/groups/create-edit-form/create-edit-form-groups-members-admin.tsx index 5d8b077c4..7d71e1855 100644 --- a/packages/frontend/src/views/admin/views/members/groups/create-edit-form/create-edit-form-groups-members-admin.tsx +++ b/packages/frontend/src/views/admin/views/members/groups/create-edit-form/create-edit-form-groups-members-admin.tsx @@ -110,7 +110,7 @@ export const CreateEditFormGroupsMembersAdmin = ({ type: 'number', className: 'max-w-32', min: 0, - disabled: values.content?.files_total_max_storage === -1, + disabled: values.content?.files_total_max_storage === 0, } as AutoFormInputProps, label: t('create_edit.files.total_max_storage'), className: 'flex flex-wrap items-center gap-2', @@ -121,16 +121,16 @@ export const CreateEditFormGroupsMembersAdmin = ({
{tCore('or')} { - if (field.value === -1) { - field.onChange(0); + if (field.value === 0) { + field.onChange(51200); return; } - field.onChange(-1); + field.onChange(0); }} />