From 22ae10cf8802b8b526a4ece38d7044db0927c74d Mon Sep 17 00:00:00 2001 From: "M. Palanikannan" <73993394+Palanikannan1437@users.noreply.github.com> Date: Wed, 18 Jun 2025 21:19:30 +0530 Subject: [PATCH] [WIKI-383] feat: dev wiki app (#3411) * dev: 1 * fix: tubro name * fix : floating sidebar * --wip-- [skip ci] * wip-2 * --wip-- [skip ci] * wip * fix: working wiki --------- Co-authored-by: VipinDevelops --- dev-wiki/.env.example | 12 + dev-wiki/.eslintignore | 3 + dev-wiki/.eslintrc.js | 6 + dev-wiki/.prettierignore | 5 + dev-wiki/.prettierrc | 5 + dev-wiki/.yarnrc.yml | 4 + dev-wiki/Dockerfile.dev | 12 + dev-wiki/Dockerfile.web | 112 ++ .../pages/(pageType)/archived/layout.tsx | 17 + .../pages/(pageType)/archived/page.tsx | 8 + .../pages/(pageType)/filters-header.tsx | 55 + .../(pages)/pages/(pageType)/header.tsx | 130 ++ .../pages/(pageType)/private/layout.tsx | 17 + .../(pages)/pages/(pageType)/private/page.tsx | 8 + .../pages/(pageType)/public/layout.tsx | 17 + .../(pages)/pages/(pageType)/public/page.tsx | 8 + .../(pages)/pages/[pageId]/header.tsx | 231 +++ .../(pages)/pages/[pageId]/layout.tsx | 15 + .../(pages)/pages/[pageId]/page.tsx | 193 ++ .../[workspaceSlug]/(pages)/pages/header.tsx | 48 + .../[workspaceSlug]/(pages)/pages/layout.tsx | 52 + .../[workspaceSlug]/(pages)/pages/page.tsx | 41 + .../[workspaceSlug]/(pages)/pages/sidebar.tsx | 112 ++ dev-wiki/app/(all)/[workspaceSlug]/layout.tsx | 3 + dev-wiki/app/(all)/layout.preload.tsx | 28 + dev-wiki/app/(all)/layout.tsx | 31 + dev-wiki/app/error.tsx | 77 + dev-wiki/app/global-error.tsx | 17 + dev-wiki/app/layout.tsx | 105 + dev-wiki/app/not-found.tsx | 46 + dev-wiki/app/provider.tsx | 53 + .../ce/components/pages/extra-actions.tsx | 8 + dev-wiki/ce/layouts/workspace-wrapper.tsx | 15 + .../core/components/auth-screens/index.ts | 2 + .../auth-screens/not-authorized-view.tsx | 37 + .../auth-screens/workspace/index.ts | 1 + .../auth-screens/workspace/not-a-member.tsx | 35 + .../core/components/common/access-field.tsx | 53 + .../common/activity/activity-block.tsx | 56 + .../common/activity/activity-item.tsx | 32 + .../components/common/activity/helper.tsx | 281 +++ .../core/components/common/activity/index.ts | 1 + .../core/components/common/activity/user.tsx | 37 + .../common/applied-filters/date.tsx | 56 + .../common/applied-filters/index.ts | 2 + .../common/applied-filters/members.tsx | 55 + .../components/common/breadcrumb-link.tsx | 44 + .../core/components/common/count-chip.tsx | 25 + .../core/components/common/custom-error.tsx | 43 + .../core/components/common/empty-state.tsx | 50 + .../components/common/filters/created-at.tsx | 76 + .../components/common/filters/created-by.tsx | 110 ++ .../core/components/common/filters/index.ts | 2 + dev-wiki/core/components/common/index.ts | 11 + .../common/latest-feature-block.tsx | 40 + .../core/components/common/logo-spinner.tsx | 17 + dev-wiki/core/components/common/logo.tsx | 95 + .../components/common/new-empty-state.tsx | 114 ++ .../components/common/page-access-icon.tsx | 15 + dev-wiki/core/components/common/pro-icon.tsx | 16 + .../core/components/common/switcher-label.tsx | 27 + dev-wiki/core/components/core/app-header.tsx | 28 + .../components/core/content-overflow-HOC.tsx | 156 ++ .../core/components/core/content-wrapper.tsx | 16 + .../core/filters/date-filter-modal.tsx | 170 ++ .../core/filters/date-filter-select.tsx | 59 + .../core/components/core/filters/index.ts | 2 + .../components/core/image-picker-popover.tsx | 389 ++++ dev-wiki/core/components/core/index.ts | 7 + dev-wiki/core/components/core/list/index.ts | 2 + .../core/components/core/list/list-item.tsx | 110 ++ .../core/components/core/list/list-root.tsx | 18 + dev-wiki/core/components/core/page-title.tsx | 14 + .../components/core/render-if-visible-HOC.tsx | 86 + .../core/components/core/sidebar/index.ts | 2 + .../core/sidebar/progress-chart.tsx | 69 + .../sidebar/sidebar-menu-hamburger-toggle.tsx | 20 + .../core/sidebar/single-progress-stats.tsx | 34 + .../core/theme/color-picker-input.tsx | 129 ++ .../core/theme/custom-theme-selector.tsx | 243 +++ dev-wiki/core/components/core/theme/index.ts | 3 + .../components/core/theme/theme-switch.tsx | 83 + .../core/components/editor/embeds/index.ts | 1 + .../editor/embeds/mentions/index.ts | 1 + .../editor/embeds/mentions/root.tsx | 17 + .../editor/embeds/mentions/user.tsx | 99 + dev-wiki/core/components/editor/index.ts | 1 + dev-wiki/core/components/editor/index.tsx | 2 + .../core/components/empty-state/index.tsx | 107 + dev-wiki/core/components/instance/index.ts | 2 + .../components/instance/maintenance-view.tsx | 34 + .../components/instance/not-ready-view.tsx | 62 + .../properties/all-properties.tsx | 1 + .../components/issues/issue-layouts/utils.ts | 12 + .../components/pages/dropdowns/actions.tsx | 194 ++ .../core/components/pages/dropdowns/index.ts | 1 + .../pages/editor/breadcrumb-page-item.tsx | 129 ++ .../components/pages/editor/editor-body.tsx | 331 ++++ .../components/pages/editor/header/index.ts | 1 + .../pages/editor/header/logo-picker.tsx | 53 + .../components/pages/editor/header/root.tsx | 89 + .../core/components/pages/editor/index.ts | 6 + .../components/pages/editor/page-root.tsx | 173 ++ .../pages/editor/summary/content-browser.tsx | 68 + .../editor/summary/heading-components.tsx | 37 + .../components/pages/editor/summary/index.ts | 1 + .../core/components/pages/editor/title.tsx | 87 + .../pages/editor/toolbar/color-dropdown.tsx | 127 ++ .../components/pages/editor/toolbar/index.ts | 5 + .../pages/editor/toolbar/info-popover.tsx | 141 ++ .../pages/editor/toolbar/options-dropdown.tsx | 134 ++ .../components/pages/editor/toolbar/root.tsx | 48 + .../pages/editor/toolbar/toolbar.tsx | 167 ++ .../core/components/pages/header/actions.tsx | 37 + .../pages/header/archived-badge.tsx | 21 + .../pages/header/copy-link-control.tsx | 76 + .../pages/header/favorite-control.tsx | 31 + .../components/pages/header/offline-badge.tsx | 24 + .../components/pages/header/syncing-badge.tsx | 72 + dev-wiki/core/components/pages/index.ts | 6 + .../pages/list/applied-filters/index.ts | 1 + .../pages/list/applied-filters/root.tsx | 79 + .../pages/list/block-item-action.tsx | 92 + .../core/components/pages/list/block-root.tsx | 55 + dev-wiki/core/components/pages/list/block.tsx | 153 ++ .../components/pages/list/filters/index.ts | 1 + .../components/pages/list/filters/root.tsx | 100 + dev-wiki/core/components/pages/list/index.ts | 7 + .../core/components/pages/list/order-by.tsx | 86 + .../components/pages/list/search-input.tsx | 109 + .../components/pages/list/tab-navigation.tsx | 61 + .../core/components/pages/loaders/index.ts | 2 + .../pages/loaders/page-content-loader.tsx | 85 + .../components/pages/loaders/page-loader.tsx | 35 + .../pages/modals/create-page-modal.tsx | 84 + .../pages/modals/delete-page-modal.tsx | 90 + .../core/components/pages/modals/index.ts | 3 + .../components/pages/modals/page-form.tsx | 158 ++ .../core/components/pages/version/editor.tsx | 148 ++ .../core/components/pages/version/index.ts | 6 + .../components/pages/version/main-content.tsx | 130 ++ .../core/components/pages/version/root.tsx | 72 + .../pages/version/sidebar-list-item.tsx | 51 + .../components/pages/version/sidebar-list.tsx | 99 + .../components/pages/version/sidebar-root.tsx | 38 + dev-wiki/core/components/sidebar/index.ts | 1 + .../components/sidebar/sidebar-navigation.tsx | 29 + dev-wiki/core/components/ui/index.ts | 1 + .../core/components/ui/view-list-loader.tsx | 20 + dev-wiki/core/components/user/index.ts | 1 + .../core/components/user/user-greetings.tsx | 52 + .../components/workspace/billing/index.ts | 1 + .../workspace/billing/subscription.ts | 213 ++ dev-wiki/core/components/workspace/index.ts | 3 + dev-wiki/core/components/workspace/logo.tsx | 37 + .../workspace/sidebar/dropdown-item.tsx | 120 ++ .../components/workspace/sidebar/dropdown.tsx | 259 +++ .../sidebar/favorites/favorite-folder.tsx | 320 +++ .../common/favorite-item-drag-handle.tsx | 41 + .../common/favorite-item-quick-action.tsx | 54 + .../common/favorite-item-title.tsx | 39 + .../common/favorite-item-wrapper.tsx | 34 + .../favorite-items/common/helper.tsx | 45 + .../favorites/favorite-items/common/index.ts | 5 + .../sidebar/favorites/favorite-items/index.ts | 2 + .../sidebar/favorites/favorite-items/root.tsx | 149 ++ .../sidebar/favorites/favorites-menu.tsx | 329 +++ .../sidebar/favorites/favorites.helpers.ts | 68 + .../workspace/sidebar/favorites/index.ts | 5 + .../sidebar/favorites/new-fav-folder.tsx | 149 ++ .../components/workspace/sidebar/index.ts | 5 + .../sidebar/workspace-menu-header.tsx | 116 ++ .../workspace/sidebar/workspace-menu-item.tsx | 83 + .../workspace/sidebar/workspace-menu.tsx | 106 + dev-wiki/core/constants/editor.ts | 390 ++++ dev-wiki/core/hooks/editor/index.ts | 2 + .../core/hooks/editor/use-editor-config.ts | 100 + .../core/hooks/editor/use-editor-mention.tsx | 76 + dev-wiki/core/hooks/store/index.ts | 7 + dev-wiki/core/hooks/store/use-app-theme.ts | 10 + dev-wiki/core/hooks/store/use-editor-asset.ts | 10 + dev-wiki/core/hooks/store/use-favorite.ts | 10 + dev-wiki/core/hooks/store/use-instance.ts | 10 + dev-wiki/core/hooks/store/use-member.ts | 11 + .../core/hooks/store/use-router-params.ts | 10 + dev-wiki/core/hooks/store/use-workspace.ts | 11 + dev-wiki/core/hooks/store/user/index.ts | 4 + .../core/hooks/store/user/user-permissions.ts | 12 + .../hooks/store/user/user-user-profile.ts | 11 + .../hooks/store/user/user-user-settings.ts | 11 + dev-wiki/core/hooks/store/user/user-user.ts | 11 + dev-wiki/core/hooks/use-app-router.tsx | 4 + dev-wiki/core/hooks/use-auto-save.tsx | 73 + .../hooks/use-collaborative-page-actions.tsx | 84 + dev-wiki/core/hooks/use-current-time.tsx | 17 + dev-wiki/core/hooks/use-dropdown-key-down.tsx | 36 + dev-wiki/core/hooks/use-keypress.tsx | 19 + dev-wiki/core/hooks/use-local-storage.tsx | 58 + dev-wiki/core/hooks/use-online-status.ts | 22 + dev-wiki/core/hooks/use-page-fallback.ts | 55 + dev-wiki/core/hooks/use-page-filters.ts | 116 ++ dev-wiki/core/hooks/use-page-operations.tsx | 216 ++ dev-wiki/core/hooks/use-platform-os.tsx | 20 + dev-wiki/core/hooks/use-query-params.ts | 39 + .../core/hooks/use-realtime-page-events.tsx | 291 +++ .../core/hooks/use-reload-confirmation.tsx | 61 + .../core/hooks/use-resolved-asset-path.tsx | 26 + dev-wiki/core/layouts/access/index.ts | 1 + .../core/layouts/access/workspace-wrapper.tsx | 26 + dev-wiki/core/layouts/auth-layout/index.ts | 1 + .../layouts/auth-layout/workspace-wrapper.tsx | 196 ++ .../core/layouts/default-layout/index.tsx | 16 + .../core/lib/n-progress/AppProgressBar.tsx | 303 +++ dev-wiki/core/lib/n-progress/index.tsx | 68 + .../lib/n-progress/utils/getAnchorProperty.ts | 59 + dev-wiki/core/lib/n-progress/utils/sameURL.ts | 16 + dev-wiki/core/lib/n-progress/withSuspense.tsx | 11 + dev-wiki/core/lib/polyfills/index.ts | 1 + .../core/lib/polyfills/requestIdleCallback.ts | 24 + dev-wiki/core/lib/store-context.tsx | 20 + .../lib/wrappers/authentication-wrapper.tsx | 139 ++ dev-wiki/core/lib/wrappers/index.ts | 2 + .../core/lib/wrappers/instance-wrapper.tsx | 42 + dev-wiki/core/lib/wrappers/store-wrapper.tsx | 67 + dev-wiki/core/services/ai.service.ts | 44 + dev-wiki/core/services/api.service.ts | 57 + dev-wiki/core/services/api_token.service.ts | 41 + dev-wiki/core/services/auth.service.ts | 78 + .../services/favorite/favorite.service.ts | 56 + dev-wiki/core/services/favorite/index.ts | 1 + dev-wiki/core/services/file-upload.service.ts | 39 + dev-wiki/core/services/file.service.ts | 342 ++++ dev-wiki/core/services/instance.service.ts | 37 + dev-wiki/core/services/user.service.ts | 264 +++ dev-wiki/core/services/webhook.service.ts | 60 + dev-wiki/core/services/workspace.service.ts | 386 ++++ dev-wiki/core/store/editor/asset.store.ts | 128 ++ dev-wiki/core/store/favorite.store.ts | 375 ++++ dev-wiki/core/store/instance.store.ts | 95 + dev-wiki/core/store/member/index.ts | 47 + .../store/member/workspace-member.store.ts | 324 +++ dev-wiki/core/store/pages/base-page.ts | 660 +++++++ .../core/store/pages/project-page.store.ts | 2 + dev-wiki/core/store/root.store.ts | 52 + dev-wiki/core/store/router.store.ts | 176 ++ dev-wiki/core/store/theme.store.ts | 162 ++ dev-wiki/core/store/user/account.store.ts | 46 + .../core/store/user/base-permissions.store.ts | 334 ++++ dev-wiki/core/store/user/index.ts | 301 +++ dev-wiki/core/store/user/profile.store.ts | 239 +++ dev-wiki/core/store/user/settings.store.ts | 101 + .../core/store/workspace/api-token.store.ts | 114 ++ dev-wiki/core/store/workspace/home.ts | 145 ++ dev-wiki/core/store/workspace/index.ts | 273 +++ dev-wiki/core/store/workspace/link.store.ts | 127 ++ .../core/store/workspace/webhook.store.ts | 196 ++ .../ee/components/feature-flags/index.tsx | 1 + .../feature-flags/with-feature-flag-hoc.tsx | 20 + dev-wiki/ee/components/global/index.ts | 1 + dev-wiki/ee/components/global/index.tsx | 0 .../ee/components/global/version-number.tsx | 20 + dev-wiki/ee/components/instance/index.ts | 1 + .../instance/maintenance-message.tsx | 6 + dev-wiki/ee/components/issues/index.tsx | 2 + .../components/pages/badge/archived-badge.tsx | 6 + dev-wiki/ee/components/pages/badge/badge.tsx | 6 + dev-wiki/ee/components/pages/badge/index.ts | 3 + .../ee/components/pages/badge/moved-badge.tsx | 3 + .../components/pages/collaborators-list.tsx | 123 ++ .../pages/editor/ai/ask-pi-menu.tsx | 112 ++ .../ee/components/pages/editor/ai/index.ts | 2 + .../ee/components/pages/editor/ai/menu.tsx | 309 +++ .../ee/components/pages/editor/embed/index.ts | 2 + .../editor/embed/issue-embed-upgrade-card.tsx | 34 + .../pages/editor/embed/issue-embed.tsx | 117 ++ dev-wiki/ee/components/pages/editor/index.ts | 2 + .../ee/components/pages/embed/content.tsx | 362 ++++ dev-wiki/ee/components/pages/embed/index.ts | 3 + .../ee/components/pages/embed/preview.tsx | 244 +++ dev-wiki/ee/components/pages/embed/root.tsx | 33 + .../ee/components/pages/extra-actions.tsx | 79 + .../pages/header/collaborators-list.tsx | 10 + .../components/pages/header/lock-control.tsx | 271 +++ dev-wiki/ee/components/pages/index.ts | 11 + .../pages/list/block-item-action.tsx | 89 + dev-wiki/ee/components/pages/list/block.tsx | 79 + dev-wiki/ee/components/pages/list/index.ts | 3 + dev-wiki/ee/components/pages/list/root.tsx | 180 ++ .../pages/modals/confirmation-modal.tsx | 69 + .../pages/modals/create-page-modal.tsx | 73 + .../pages/modals/delete-page-modal.tsx | 79 + dev-wiki/ee/components/pages/modals/index.ts | 5 + .../pages/modals/lock-page-modal.tsx | 56 + .../modals/multiple-page-delete-modal.tsx | 134 ++ .../pages/modals/publish-page-modal.tsx | 146 ++ .../ee/components/pages/quick-actions.tsx | 112 ++ dev-wiki/ee/components/pages/sidebar/index.ts | 3 + .../pages/sidebar/list-item-root.tsx | 354 ++++ .../ee/components/pages/sidebar/list-item.tsx | 213 ++ dev-wiki/ee/components/pages/sidebar/list.tsx | 49 + dev-wiki/ee/components/pages/sidebar/menu.tsx | 75 + .../pages/sidebar/quick-actions.tsx | 43 + .../pages/sidebar/section/components/index.ts | 2 + .../section/components/section-content.tsx | 103 + .../section/components/section-header.tsx | 78 + .../pages/sidebar/section/constants.ts | 19 + .../pages/sidebar/section/hooks/index.ts | 3 + .../section/hooks/use-section-drag-drop.ts | 56 + .../section/hooks/use-section-pages.ts | 25 + .../components/pages/sidebar/section/index.ts | 5 + .../pages/sidebar/section/section-root.tsx | 350 ++++ .../components/pages/sidebar/section/types.ts | 50 + .../ee/components/pages/version/editor.tsx | 113 ++ dev-wiki/ee/components/pages/version/index.ts | 1 + .../pages/workspace-pages-upgrade.tsx | 41 + .../ee/components/sidebar/app-switcher.tsx | 84 + dev-wiki/ee/components/sidebar/index.ts | 1 + dev-wiki/ee/components/workspace-disabled.tsx | 97 + .../ee/components/workspace/edition-badge.tsx | 23 + dev-wiki/ee/components/workspace/index.ts | 1 + dev-wiki/ee/components/workspace/index.tsx | 1 + .../ee/components/workspace/sidebar/index.tsx | 1 + dev-wiki/ee/constants/ai.ts | 8 + dev-wiki/ee/constants/editor.ts | 11 + dev-wiki/ee/constants/index.ts | 1 + dev-wiki/ee/constants/sidebar-favorites.ts | 46 + dev-wiki/ee/helpers/dashboard.helper.ts | 53 + dev-wiki/ee/helpers/instance.helper.ts | 7 + .../ee/hooks/context/use-project-creation.ts | 1 + dev-wiki/ee/hooks/store/index.ts | 7 + dev-wiki/ee/hooks/store/use-feature-flags.ts | 11 + dev-wiki/ee/hooks/store/use-flag.ts | 15 + dev-wiki/ee/hooks/store/use-page-store.ts | 25 + dev-wiki/ee/hooks/store/use-page.ts | 23 + dev-wiki/ee/hooks/store/use-publish-page.ts | 11 + .../store/use-self-hosted-subscription.ts | 11 + .../ee/hooks/store/use-workspace-features.ts | 11 + .../hooks/use-additional-editor-mention.tsx | 41 + .../use-additional-favorite-item-details.ts | 37 + dev-wiki/ee/hooks/use-editor-embed.tsx | 197 ++ dev-wiki/ee/hooks/use-editor-flagging.ts | 36 + dev-wiki/ee/hooks/use-file-size.ts | 34 + dev-wiki/ee/hooks/use-issue-embed.tsx | 82 + dev-wiki/ee/hooks/use-page-actions-menu.tsx | 128 ++ dev-wiki/ee/hooks/use-page-flag.ts | 20 + .../ee/hooks/use-workspace-subscription.ts | 11 + dev-wiki/ee/layouts/workspace-wrapper.tsx | 69 + dev-wiki/ee/services/feature-flag.service.ts | 26 + dev-wiki/ee/services/index.ts | 2 + dev-wiki/ee/services/page/index.ts | 2 + .../ee/services/page/publish-page.service.ts | 100 + .../page/workspace-page-version.service.ts | 36 + .../services/page/workspace-page.service.ts | 175 ++ dev-wiki/ee/services/payment.service.ts | 130 ++ .../self-hosted-subscription.service.ts | 88 + .../ee/services/workspace-feature.service.ts | 50 + dev-wiki/ee/services/workspace.service.ts | 63 + .../feature-flags/feature-flags.store.ts | 64 + dev-wiki/ee/store/pages/publish-page.store.ts | 130 ++ .../ee/store/pages/workspace-page.store.ts | 725 +++++++ dev-wiki/ee/store/pages/workspace-page.ts | 237 +++ dev-wiki/ee/store/root.store.ts | 45 + .../self-hosted-subscription.store.ts | 159 ++ .../store/subscription/subscription.store.ts | 278 +++ dev-wiki/ee/store/user/permission.store.ts | 52 + dev-wiki/ee/store/workspace-feature.store.ts | 133 ++ dev-wiki/ee/types/app.d.ts | 9 + dev-wiki/ee/types/index.ts | 1 + dev-wiki/ee/types/pages.d.ts | 8 + dev-wiki/ee/types/self-hosted-subscription.ts | 14 + dev-wiki/ee/types/workspace-feature.ts | 22 + dev-wiki/google.d.ts | 91 + dev-wiki/helpers/array.helper.ts | 119 ++ dev-wiki/helpers/authentication.helper.tsx | 450 +++++ dev-wiki/helpers/color.helper.ts | 144 ++ dev-wiki/helpers/common.helper.ts | 45 + dev-wiki/helpers/date-time.helper.ts | 477 +++++ dev-wiki/helpers/editor.helper.ts | 128 ++ dev-wiki/helpers/emoji.helper.tsx | 77 + dev-wiki/helpers/file.helper.ts | 96 + dev-wiki/helpers/filter.helper.ts | 98 + dev-wiki/helpers/page.helper.ts | 85 + dev-wiki/helpers/string.helper.ts | 254 +++ dev-wiki/helpers/tab-indices.helper.ts | 10 + dev-wiki/helpers/theme.helper.ts | 123 ++ dev-wiki/helpers/user.helper.ts | 12 + dev-wiki/manifest.json | 32 + dev-wiki/next-env.d.ts | 5 + dev-wiki/next.config.js | 133 ++ dev-wiki/null | 0 dev-wiki/package.json | 109 + dev-wiki/postcss.config.js | 2 + .../.well-known/apple-app-site-association | 19 + dev-wiki/public/.well-known/assetlinks.json | 32 + dev-wiki/public/404.svg | 17 + dev-wiki/public/animated-icons/uploading.json | 1 + dev-wiki/public/attachment/audio-icon.png | Bin 0 -> 10369 bytes dev-wiki/public/attachment/css-icon.png | Bin 0 -> 12703 bytes dev-wiki/public/attachment/csv-icon.png | Bin 0 -> 9681 bytes dev-wiki/public/attachment/default-icon.png | Bin 0 -> 6134 bytes dev-wiki/public/attachment/doc-icon.png | Bin 0 -> 15175 bytes dev-wiki/public/attachment/excel-icon.png | Bin 0 -> 6708 bytes dev-wiki/public/attachment/figma-icon.png | Bin 0 -> 15614 bytes dev-wiki/public/attachment/html-icon.png | Bin 0 -> 6665 bytes dev-wiki/public/attachment/img-icon.png | Bin 0 -> 10447 bytes dev-wiki/public/attachment/jpg-icon.png | Bin 0 -> 12088 bytes dev-wiki/public/attachment/js-icon.png | Bin 0 -> 10992 bytes dev-wiki/public/attachment/pdf-icon.png | Bin 0 -> 13038 bytes dev-wiki/public/attachment/png-icon.png | Bin 0 -> 12486 bytes dev-wiki/public/attachment/rar-icon.png | Bin 0 -> 10465 bytes dev-wiki/public/attachment/svg-icon.png | Bin 0 -> 12899 bytes dev-wiki/public/attachment/txt-icon.png | Bin 0 -> 11142 bytes dev-wiki/public/attachment/video-icon.png | Bin 0 -> 11673 bytes dev-wiki/public/attachment/zip-icon.png | Bin 0 -> 7217 bytes dev-wiki/public/auth/access-denied.svg | 49 + .../public/auth/background-pattern-dark.svg | 68 + dev-wiki/public/auth/background-pattern.svg | 68 + .../public/auth/project-not-authorized.svg | 44 + dev-wiki/public/auth/unauthorized.svg | 44 + .../public/auth/workspace-not-authorized.svg | 44 + dev-wiki/public/emoji/project-emoji.svg | 4 + .../active-cycle/assignee-dark.webp | Bin 0 -> 1188 bytes .../active-cycle/assignee-light.webp | Bin 0 -> 1156 bytes .../empty-state/active-cycle/chart-dark.webp | Bin 0 -> 1176 bytes .../empty-state/active-cycle/chart-light.webp | Bin 0 -> 1154 bytes .../empty-state/active-cycle/cycle-dark.webp | Bin 0 -> 1360 bytes .../empty-state/active-cycle/cycle-light.webp | Bin 0 -> 1246 bytes .../empty-state/active-cycle/label-dark.webp | Bin 0 -> 1138 bytes .../empty-state/active-cycle/label-light.webp | Bin 0 -> 1186 bytes .../active-cycle/priority-dark.webp | Bin 0 -> 1052 bytes .../active-cycle/priority-light.webp | Bin 0 -> 1096 bytes .../active-cycle/progress-dark.webp | Bin 0 -> 1094 bytes .../active-cycle/progress-light.webp | Bin 0 -> 1172 bytes .../all-issues/all-issues-dark.webp | Bin 0 -> 85642 bytes .../all-issues/all-issues-light.webp | Bin 0 -> 71872 bytes .../empty-state/all-issues/assigned-dark.webp | Bin 0 -> 70338 bytes .../all-issues/assigned-light.webp | Bin 0 -> 72084 bytes .../empty-state/all-issues/created-dark.webp | Bin 0 -> 70004 bytes .../empty-state/all-issues/created-light.webp | Bin 0 -> 72328 bytes .../all-issues/custom-view-dark.webp | Bin 0 -> 70366 bytes .../all-issues/custom-view-light.webp | Bin 0 -> 72264 bytes .../all-issues/no-project-dark.webp | Bin 0 -> 85516 bytes .../all-issues/no-project-light.webp | Bin 0 -> 68070 bytes .../all-issues/subscribed-dark.webp | Bin 0 -> 70354 bytes .../all-issues/subscribed-light.webp | Bin 0 -> 72268 bytes .../analytics/empty-chart-area-dark.webp | Bin 0 -> 2720 bytes .../analytics/empty-chart-area-light.webp | Bin 0 -> 694 bytes .../analytics/empty-chart-bar-dark.webp | Bin 0 -> 2508 bytes .../analytics/empty-chart-bar-light.webp | Bin 0 -> 512 bytes .../analytics/empty-chart-radar-dark.webp | Bin 0 -> 3076 bytes .../analytics/empty-chart-radar-light.webp | Bin 0 -> 716 bytes .../analytics/empty-grid-background-dark.webp | Bin 0 -> 35070 bytes .../empty-grid-background-light.webp | Bin 0 -> 2576 bytes .../analytics/empty-table-dark.webp | Bin 0 -> 3280 bytes .../analytics/empty-table-light.webp | Bin 0 -> 862 bytes dev-wiki/public/empty-state/api-token.svg | 49 + .../archived/empty-cycles-dark.webp | Bin 0 -> 47726 bytes .../archived/empty-cycles-light.webp | Bin 0 -> 58406 bytes .../archived/empty-issues-dark.webp | Bin 0 -> 54934 bytes .../archived/empty-issues-light.webp | Bin 0 -> 65014 bytes .../archived/empty-modules-dark.webp | Bin 0 -> 43128 bytes .../archived/empty-modules-light.webp | Bin 0 -> 45046 bytes .../customers/customer-upgrade-dark.webp | Bin 0 -> 26688 bytes .../customers/customer-upgrade-light.webp | Bin 0 -> 27060 bytes .../public/empty-state/customers/customer.svg | 3 + .../customers/customers-disabled-dark.webp | Bin 0 -> 24700 bytes .../customers/customers-disabled-light.webp | Bin 0 -> 26142 bytes .../empty-state/customers/detail-dark.svg | 42 + .../empty-state/customers/detail-light.svg | 40 + .../customers/request-search-dark.svg | 39 + .../customers/request-search-light.svg | 39 + .../customers/search-empty-dark.svg | 39 + .../customers/search-empty-light.svg | 39 + .../cycle-issues/calendar-dark-resp.webp | Bin 0 -> 37114 bytes .../cycle-issues/calendar-dark.webp | Bin 0 -> 57398 bytes .../cycle-issues/calendar-light-resp.webp | Bin 0 -> 39002 bytes .../cycle-issues/calendar-light.webp | Bin 0 -> 60846 bytes .../cycle-issues/gantt_chart-dark-resp.webp | Bin 0 -> 44260 bytes .../cycle-issues/gantt_chart-dark.webp | Bin 0 -> 69388 bytes .../cycle-issues/gantt_chart-light-resp.webp | Bin 0 -> 44350 bytes .../cycle-issues/gantt_chart-light.webp | Bin 0 -> 75124 bytes .../cycle-issues/kanban-dark-resp.webp | Bin 0 -> 58980 bytes .../empty-state/cycle-issues/kanban-dark.webp | Bin 0 -> 93938 bytes .../cycle-issues/kanban-light-resp.webp | Bin 0 -> 60980 bytes .../cycle-issues/kanban-light.webp | Bin 0 -> 100208 bytes .../cycle-issues/list-dark-resp.webp | Bin 0 -> 47436 bytes .../empty-state/cycle-issues/list-dark.webp | Bin 0 -> 75056 bytes .../cycle-issues/list-light-resp.webp | Bin 0 -> 48962 bytes .../empty-state/cycle-issues/list-light.webp | Bin 0 -> 79588 bytes .../cycle-issues/spreadsheet-dark-resp.webp | Bin 0 -> 50896 bytes .../cycle-issues/spreadsheet-dark.webp | Bin 0 -> 83280 bytes .../cycle-issues/spreadsheet-light-resp.webp | Bin 0 -> 52632 bytes .../cycle-issues/spreadsheet-light.webp | Bin 0 -> 84346 bytes dev-wiki/public/empty-state/cycle.svg | 21 + .../public/empty-state/cycle/active-dark.webp | Bin 0 -> 70616 bytes .../empty-state/cycle/active-light.webp | Bin 0 -> 71370 bytes .../public/empty-state/cycle/all-filters.svg | 42 + .../empty-state/cycle/completed-dark.webp | Bin 0 -> 79182 bytes .../empty-state/cycle/completed-light.webp | Bin 0 -> 85086 bytes .../cycle/completed-no-issues-dark.webp | Bin 0 -> 88100 bytes .../cycle/completed-no-issues-light.webp | Bin 0 -> 90250 bytes .../public/empty-state/cycle/draft-dark.webp | Bin 0 -> 67770 bytes .../public/empty-state/cycle/draft-light.webp | Bin 0 -> 71682 bytes .../public/empty-state/cycle/name-filter.svg | 41 + .../empty-state/cycle/upcoming-dark.webp | Bin 0 -> 77842 bytes .../empty-state/cycle/upcoming-light.webp | Bin 0 -> 83712 bytes .../dashboard/dark/completed-issues.svg | 5 + .../dashboard/dark/issues-by-priority.svg | 6 + .../dashboard/dark/issues-by-state-group.svg | 24 + .../dashboard/dark/overdue-issues.svg | 9 + .../dashboard/dark/recent-activity.svg | 6 + .../dashboard/dark/recent-collaborators-1.svg | 6 + .../dashboard/dark/recent-collaborators-2.svg | 6 + .../dashboard/dark/recent-collaborators-3.svg | 6 + .../dashboard/dark/upcoming-issues.svg | 13 + .../dashboard/light/completed-issues.svg | 5 + .../dashboard/light/issues-by-priority.svg | 6 + .../dashboard/light/issues-by-state-group.svg | 27 + .../dashboard/light/overdue-issues.svg | 9 + .../dashboard/light/recent-activity.svg | 6 + .../light/recent-collaborators-1.svg | 7 + .../light/recent-collaborators-2.svg | 7 + .../light/recent-collaborators-3.svg | 7 + .../dashboard/light/upcoming-issues.svg | 13 + .../empty-state/dashboard/widgets-dark.webp | Bin 0 -> 78084 bytes .../empty-state/dashboard/widgets-light.webp | Bin 0 -> 67128 bytes .../empty-state/dashboard_empty_project.webp | Bin 0 -> 31886 bytes .../dashboards/feature-flag/inside-dark.webp | Bin 0 -> 75944 bytes .../dashboards/feature-flag/inside-light.webp | Bin 0 -> 76988 bytes .../dashboards/feature-flag/outside-dark.webp | Bin 0 -> 35934 bytes .../feature-flag/outside-light.webp | Bin 0 -> 36022 bytes .../empty-state/dashboards/list-dark.webp | Bin 0 -> 92992 bytes .../empty-state/dashboards/list-light.webp | Bin 0 -> 85602 bytes .../dashboards/list-search-dark.webp | Bin 0 -> 64116 bytes .../dashboards/list-search-light.webp | Bin 0 -> 77146 bytes .../widgets/charts/area_chart-dark.webp | Bin 0 -> 2084 bytes .../widgets/charts/area_chart-light.webp | Bin 0 -> 2388 bytes .../widgets/charts/bar_chart-dark.webp | Bin 0 -> 2096 bytes .../widgets/charts/bar_chart-light.webp | Bin 0 -> 2380 bytes .../widgets/charts/donut_chart-dark.webp | Bin 0 -> 2528 bytes .../widgets/charts/donut_chart-light.webp | Bin 0 -> 3138 bytes .../widgets/charts/line_chart-dark.webp | Bin 0 -> 2126 bytes .../widgets/charts/line_chart-light.webp | Bin 0 -> 2442 bytes .../widgets/charts/number-dark.webp | Bin 0 -> 2682 bytes .../widgets/charts/number-light.webp | Bin 0 -> 3594 bytes .../widgets/charts/pie_chart-dark.webp | Bin 0 -> 2312 bytes .../widgets/charts/pie_chart-light.webp | Bin 0 -> 2772 bytes .../dashboards/widgets/list-dark.webp | Bin 0 -> 61964 bytes .../dashboards/widgets/list-light.webp | Bin 0 -> 75226 bytes .../disabled-feature/cycles-dark.webp | Bin 0 -> 71882 bytes .../disabled-feature/cycles-light.webp | Bin 0 -> 74068 bytes .../disabled-feature/intake-dark.webp | Bin 0 -> 83506 bytes .../disabled-feature/intake-light.webp | Bin 0 -> 87120 bytes .../disabled-feature/modules-dark.webp | Bin 0 -> 64292 bytes .../disabled-feature/modules-light.webp | Bin 0 -> 67546 bytes .../disabled-feature/pages-dark.webp | Bin 0 -> 83862 bytes .../disabled-feature/pages-light.webp | Bin 0 -> 74942 bytes .../disabled-feature/views-dark.webp | Bin 0 -> 55062 bytes .../disabled-feature/views-light.webp | Bin 0 -> 55700 bytes .../draft/draft-issues-empty-dark.webp | Bin 0 -> 53896 bytes .../draft/draft-issues-empty-light.webp | Bin 0 -> 57020 bytes .../empty-filters/calendar-dark.webp | Bin 0 -> 64996 bytes .../empty-filters/calendar-light.webp | Bin 0 -> 66142 bytes .../empty-filters/gantt_chart-dark.webp | Bin 0 -> 71714 bytes .../empty-filters/gantt_chart-light.webp | Bin 0 -> 72198 bytes .../empty-filters/kanban-dark.webp | Bin 0 -> 98618 bytes .../empty-filters/kanban-light.webp | Bin 0 -> 100864 bytes .../empty-state/empty-filters/list-dark.webp | Bin 0 -> 79200 bytes .../empty-state/empty-filters/list-light.webp | Bin 0 -> 80098 bytes .../empty-filters/spreadsheet-dark.webp | Bin 0 -> 87120 bytes .../empty-filters/spreadsheet-light.webp | Bin 0 -> 87834 bytes .../empty-state/empty-updates-light.png | Bin 0 -> 104151 bytes .../public/empty-state/empty_analytics.webp | Bin 0 -> 12182 bytes .../public/empty-state/empty_bar_graph.svg | 5 + dev-wiki/public/empty-state/empty_cycles.webp | Bin 0 -> 27484 bytes dev-wiki/public/empty-state/empty_graph.svg | 10 + dev-wiki/public/empty-state/empty_issues.webp | Bin 0 -> 60102 bytes dev-wiki/public/empty-state/empty_label.svg | 4 + dev-wiki/public/empty-state/empty_members.svg | 13 + .../public/empty-state/empty_modules.webp | Bin 0 -> 18618 bytes dev-wiki/public/empty-state/empty_page.png | Bin 0 -> 62834 bytes .../public/empty-state/empty_project.webp | Bin 0 -> 35082 bytes dev-wiki/public/empty-state/empty_users.svg | 3 + dev-wiki/public/empty-state/empty_view.webp | Bin 0 -> 30928 bytes .../public/empty-state/epics/epics-dark.webp | Bin 0 -> 48054 bytes .../public/empty-state/epics/epics-light.webp | Bin 0 -> 50384 bytes .../empty-state/epics/settings-dark.png | Bin 0 -> 44713 bytes .../empty-state/epics/settings-light.png | Bin 0 -> 42955 bytes .../public/empty-state/estimates/dark.svg | 19 + .../public/empty-state/estimates/light.svg | 19 + .../initiatives/initiatives-dark.webp | Bin 0 -> 73388 bytes .../initiatives/initiatives-light.webp | Bin 0 -> 64678 bytes .../empty-state/initiatives/upgrade-dark.webp | Bin 0 -> 14740 bytes .../initiatives/upgrade-light.webp | Bin 0 -> 15620 bytes .../empty-state/intake/filter-issue-dark.webp | Bin 0 -> 46360 bytes .../intake/filter-issue-light.webp | Bin 0 -> 44474 bytes .../empty-state/intake/intake-dark-resp.webp | Bin 0 -> 54190 bytes .../empty-state/intake/intake-dark.webp | Bin 0 -> 83506 bytes .../empty-state/intake/intake-issue-dark.webp | Bin 0 -> 46760 bytes .../intake/intake-issue-light.webp | Bin 0 -> 44138 bytes .../empty-state/intake/intake-light-resp.webp | Bin 0 -> 58260 bytes .../empty-state/intake/intake-light.webp | Bin 0 -> 87120 bytes .../empty-state/intake/issue-detail-dark.webp | Bin 0 -> 2170 bytes .../intake/issue-detail-light.webp | Bin 0 -> 2264 bytes dev-wiki/public/empty-state/invitation.svg | 23 + .../issue-types/issue-type-dark.png | Bin 0 -> 219294 bytes .../issue-types/issue-type-dark.svg | 810 ++++++++ .../issue-types/issue-type-light.png | Bin 0 -> 244039 bytes .../issue-types/issue-type-light.svg | 192 ++ dev-wiki/public/empty-state/issue.svg | 15 + dev-wiki/public/empty-state/label.svg | 42 + .../marketplace/empty-workspace-dark.png | Bin 0 -> 94046 bytes .../marketplace/empty-workspace-light.png | Bin 0 -> 250735 bytes .../module-issues/calendar-dark-resp.webp | Bin 0 -> 36396 bytes .../module-issues/calendar-dark.webp | Bin 0 -> 56800 bytes .../module-issues/calendar-light-resp.webp | Bin 0 -> 38550 bytes .../module-issues/calendar-light.webp | Bin 0 -> 64242 bytes .../module-issues/gantt_chart-dark-resp.webp | Bin 0 -> 43940 bytes .../module-issues/gantt_chart-dark.webp | Bin 0 -> 68556 bytes .../module-issues/gantt_chart-light-resp.webp | Bin 0 -> 43636 bytes .../module-issues/gantt_chart-light.webp | Bin 0 -> 70102 bytes .../module-issues/kanban-dark-resp.webp | Bin 0 -> 58420 bytes .../module-issues/kanban-dark.webp | Bin 0 -> 93376 bytes .../module-issues/kanban-light-resp.webp | Bin 0 -> 59474 bytes .../module-issues/kanban-light.webp | Bin 0 -> 99462 bytes .../module-issues/list-dark-resp.webp | Bin 0 -> 47000 bytes .../empty-state/module-issues/list-dark.webp | Bin 0 -> 74544 bytes .../module-issues/list-light-resp.webp | Bin 0 -> 48404 bytes .../empty-state/module-issues/list-light.webp | Bin 0 -> 78682 bytes .../module-issues/spreadsheet-dark-resp.webp | Bin 0 -> 50794 bytes .../module-issues/spreadsheet-dark.webp | Bin 0 -> 78710 bytes .../module-issues/spreadsheet-light-resp.webp | Bin 0 -> 51730 bytes .../module-issues/spreadsheet-light.webp | Bin 0 -> 83500 bytes dev-wiki/public/empty-state/module.svg | 31 + .../public/empty-state/module/all-filters.svg | 45 + .../public/empty-state/module/name-filter.svg | 44 + dev-wiki/public/empty-state/notification.svg | 38 + .../onboarding/analytics-dark.webp | Bin 0 -> 54378 bytes .../onboarding/analytics-light.webp | Bin 0 -> 47910 bytes .../empty-state/onboarding/archive-dark.png | Bin 0 -> 6690 bytes .../empty-state/onboarding/archive-light.png | Bin 0 -> 6723 bytes .../empty-state/onboarding/cycles-dark.webp | Bin 0 -> 71690 bytes .../empty-state/onboarding/cycles-light.webp | Bin 0 -> 74002 bytes .../onboarding/dashboard-dark.webp | Bin 0 -> 72264 bytes .../onboarding/dashboard-light.webp | Bin 0 -> 73042 bytes .../empty-state/onboarding/graph-dark.png | Bin 0 -> 5298 bytes .../empty-state/onboarding/graph-light.png | Bin 0 -> 5318 bytes .../onboarding/issues-closed-dark.png | Bin 0 -> 5846 bytes .../onboarding/issues-closed-light.png | Bin 0 -> 5880 bytes .../empty-state/onboarding/issues-dark.webp | Bin 0 -> 85274 bytes .../empty-state/onboarding/issues-light.webp | Bin 0 -> 88902 bytes .../empty-state/onboarding/members-dark.png | Bin 0 -> 7225 bytes .../empty-state/onboarding/members-light.png | Bin 0 -> 7173 bytes .../empty-state/onboarding/modules-dark.webp | Bin 0 -> 64280 bytes .../empty-state/onboarding/modules-light.webp | Bin 0 -> 67462 bytes .../onboarding/notification-dark.png | Bin 0 -> 7720 bytes .../onboarding/notification-light.png | Bin 0 -> 7593 bytes .../empty-state/onboarding/pages-dark.webp | Bin 0 -> 84448 bytes .../empty-state/onboarding/pages-light.webp | Bin 0 -> 215178 bytes .../empty-state/onboarding/projects-dark.webp | Bin 0 -> 100726 bytes .../onboarding/projects-light.webp | Bin 0 -> 117956 bytes .../empty-state/onboarding/search-dark.png | Bin 0 -> 8300 bytes .../empty-state/onboarding/search-light.png | Bin 0 -> 8096 bytes .../empty-state/onboarding/snooze-light.png | Bin 0 -> 8684 bytes .../empty-state/onboarding/snoozed-dark.png | Bin 0 -> 9051 bytes .../empty-state/onboarding/views-dark.webp | Bin 0 -> 55062 bytes .../empty-state/onboarding/views-light.webp | Bin 0 -> 55700 bytes .../workspace-active-cycles-dark.webp | Bin 0 -> 67144 bytes .../workspace-active-cycles-light.webp | Bin 0 -> 75088 bytes .../onboarding/workspace-invites-dark.webp | Bin 0 -> 53572 bytes .../onboarding/workspace-invites-light.webp | Bin 0 -> 65348 bytes .../public/empty-state/pages/all-dark.webp | Bin 0 -> 84604 bytes .../public/empty-state/pages/all-filters.svg | 45 + .../public/empty-state/pages/all-light.webp | Bin 0 -> 87890 bytes .../empty-state/pages/archived-dark.webp | Bin 0 -> 52588 bytes .../empty-state/pages/archived-light.webp | Bin 0 -> 54758 bytes .../public/empty-state/pages/name-filter.svg | 44 + .../empty-state/pages/private-dark.webp | Bin 0 -> 47460 bytes .../empty-state/pages/private-light.webp | Bin 0 -> 49954 bytes .../public/empty-state/pages/public-dark.webp | Bin 0 -> 47552 bytes .../empty-state/pages/public-light.webp | Bin 0 -> 50602 bytes dev-wiki/public/empty-state/pi/chat-dark.webp | Bin 0 -> 358912 bytes .../public/empty-state/pi/chat-light.webp | Bin 0 -> 376336 bytes .../empty-state/profile/activities-dark.webp | Bin 0 -> 602498 bytes .../empty-state/profile/activities-light.webp | Bin 0 -> 605508 bytes .../empty-state/profile/activity-dark.webp | Bin 0 -> 65728 bytes .../empty-state/profile/activity-light.webp | Bin 0 -> 66542 bytes .../empty-state/profile/assigned-dark.webp | Bin 0 -> 48858 bytes .../empty-state/profile/assigned-light.webp | Bin 0 -> 52468 bytes .../empty-state/profile/created-dark.webp | Bin 0 -> 48718 bytes .../empty-state/profile/created-light.webp | Bin 0 -> 53188 bytes .../profile/issues-by-priority-dark.webp | Bin 0 -> 204278 bytes .../profile/issues-by-priority-light.webp | Bin 0 -> 204198 bytes .../profile/issues-by-state-dark.webp | Bin 0 -> 217670 bytes .../profile/issues-by-state-light.webp | Bin 0 -> 217914 bytes .../empty-state/profile/subscribed-dark.webp | Bin 0 -> 48754 bytes .../empty-state/profile/subscribed-light.webp | Bin 0 -> 53274 bytes .../project-settings/estimates-dark-resp.webp | Bin 0 -> 28148 bytes .../project-settings/estimates-dark.png | Bin 0 -> 1385116 bytes .../project-settings/estimates-dark.webp | Bin 0 -> 50982 bytes .../estimates-light-resp.webp | Bin 0 -> 33026 bytes .../project-settings/estimates-light.png | Bin 0 -> 852149 bytes .../project-settings/estimates-light.webp | Bin 0 -> 55100 bytes .../integrations-dark-resp.webp | Bin 0 -> 37980 bytes .../project-settings/integrations-dark.webp | Bin 0 -> 66530 bytes .../integrations-light-resp.webp | Bin 0 -> 40660 bytes .../project-settings/integrations-light.webp | Bin 0 -> 66340 bytes .../project-settings/labels-dark-resp.webp | Bin 0 -> 33296 bytes .../project-settings/labels-dark.webp | Bin 0 -> 42628 bytes .../project-settings/labels-light-resp.webp | Bin 0 -> 28552 bytes .../project-settings/labels-light.webp | Bin 0 -> 47320 bytes .../project-settings/no-projects-dark.png | Bin 0 -> 299445 bytes .../project-settings/no-projects-light.png | Bin 0 -> 261332 bytes .../project-settings/updates-dark.png | Bin 0 -> 39599 bytes .../project-settings/updates-light.png | Bin 0 -> 39630 bytes dev-wiki/public/empty-state/project.svg | 23 + .../empty-state/project/all-filters.svg | 42 + .../empty-state/project/name-filter.svg | 41 + .../public/empty-state/recent_activity.svg | 3 + .../search/all-issue-view-dark.webp | Bin 0 -> 60420 bytes .../search/all-issues-view-light.webp | Bin 0 -> 72366 bytes .../empty-state/search/archive-dark.webp | Bin 0 -> 2292 bytes .../empty-state/search/archive-light.webp | Bin 0 -> 2434 bytes .../empty-state/search/comments-dark.webp | Bin 0 -> 2574 bytes .../empty-state/search/comments-light.webp | Bin 0 -> 2744 bytes .../empty-state/search/issues-dark.webp | Bin 0 -> 3118 bytes .../empty-state/search/issues-light.webp | Bin 0 -> 2924 bytes .../empty-state/search/member-dark.webp | Bin 0 -> 61754 bytes .../empty-state/search/member-light.webp | Bin 0 -> 74852 bytes .../empty-state/search/notification-dark.webp | Bin 0 -> 2286 bytes .../search/notification-light.webp | Bin 0 -> 2524 bytes .../empty-state/search/project-dark.webp | Bin 0 -> 52156 bytes .../empty-state/search/project-light.webp | Bin 0 -> 66366 bytes .../empty-state/search/search-dark.webp | Bin 0 -> 2330 bytes .../empty-state/search/search-light.webp | Bin 0 -> 2644 bytes .../empty-state/search/snooze-dark.webp | Bin 0 -> 2464 bytes .../empty-state/search/snooze-light.webp | Bin 0 -> 2790 bytes .../public/empty-state/search/views-dark.webp | Bin 0 -> 56238 bytes .../empty-state/search/views-light.webp | Bin 0 -> 72318 bytes dev-wiki/public/empty-state/state_graph.svg | 26 + .../empty-state/stickies/stickies-dark.webp | Bin 0 -> 123458 bytes .../empty-state/stickies/stickies-light.webp | Bin 0 -> 128460 bytes .../stickies/stickies-search-dark.webp | Bin 0 -> 85006 bytes .../stickies/stickies-search-light.webp | Bin 0 -> 72294 bytes .../teams/completed-cycles-dark.webp | Bin 0 -> 73768 bytes .../teams/completed-cycles-light.webp | Bin 0 -> 79098 bytes .../teams/current-cycles-dark.webp | Bin 0 -> 80580 bytes .../teams/current-cycles-light.webp | Bin 0 -> 86794 bytes .../empty-state/teams/dark-upgrade.webp | Bin 0 -> 30980 bytes .../public/empty-state/teams/issues-dark.webp | Bin 0 -> 128616 bytes .../empty-state/teams/issues-light.webp | Bin 0 -> 127492 bytes .../empty-state/teams/light-upgrade.webp | Bin 0 -> 33302 bytes .../empty-state/teams/projects-dark.webp | Bin 0 -> 15858 bytes .../empty-state/teams/projects-light.webp | Bin 0 -> 18454 bytes .../public/empty-state/teams/teams-dark.webp | Bin 0 -> 95636 bytes .../public/empty-state/teams/teams-light.webp | Bin 0 -> 98572 bytes .../teams/upcoming-cycles-dark.webp | Bin 0 -> 77052 bytes .../teams/upcoming-cycles-light.webp | Bin 0 -> 82030 bytes .../public/empty-state/teams/views-dark.webp | Bin 0 -> 83482 bytes .../public/empty-state/teams/views-light.webp | Bin 0 -> 85666 bytes .../templates/no-templates-dark.webp | Bin 0 -> 36738 bytes .../templates/no-templates-light.webp | Bin 0 -> 40580 bytes .../empty-state/templates/upgrade-dark.webp | Bin 0 -> 35596 bytes .../empty-state/templates/upgrade-light.webp | Bin 0 -> 37480 bytes dev-wiki/public/empty-state/view.svg | 26 + dev-wiki/public/empty-state/web-hook.svg | 49 + .../empty-state/workflows/upgrade-dark.webp | Bin 0 -> 25648 bytes .../empty-state/workflows/upgrade-light.webp | Bin 0 -> 28548 bytes .../empty-state/worklogs/worklog-dark.png | Bin 0 -> 202444 bytes .../empty-state/worklogs/worklog-light.png | Bin 0 -> 214607 bytes .../workspace-draft/issue-dark.webp | Bin 0 -> 80938 bytes .../workspace-draft/issue-light.webp | Bin 0 -> 91768 bytes .../api-tokens-dark-resp.webp | Bin 0 -> 38362 bytes .../workspace-settings/api-tokens-dark.webp | Bin 0 -> 47030 bytes .../api-tokens-light-resp.webp | Bin 0 -> 32154 bytes .../workspace-settings/api-tokens-light.webp | Bin 0 -> 51692 bytes .../workspace-settings/exports-dark-resp.webp | Bin 0 -> 44288 bytes .../workspace-settings/exports-dark.webp | Bin 0 -> 62236 bytes .../exports-light-resp.webp | Bin 0 -> 42258 bytes .../workspace-settings/exports-light.webp | Bin 0 -> 67094 bytes .../workspace-settings/imports-dark-resp.webp | Bin 0 -> 46276 bytes .../workspace-settings/imports-dark.webp | Bin 0 -> 78010 bytes .../imports-light-resp.webp | Bin 0 -> 51614 bytes .../workspace-settings/imports-light.webp | Bin 0 -> 81674 bytes .../integrations-dark-resp.webp | Bin 0 -> 37980 bytes .../workspace-settings/integrations-dark.webp | Bin 0 -> 66530 bytes .../integrations-light-resp.webp | Bin 0 -> 40660 bytes .../integrations-light.webp | Bin 0 -> 66340 bytes .../webhooks-dark-resp.webp | Bin 0 -> 40072 bytes .../workspace-settings/webhooks-dark.webp | Bin 0 -> 54446 bytes .../webhooks-light-resp.webp | Bin 0 -> 35966 bytes .../workspace-settings/webhooks-light.webp | Bin 0 -> 52944 bytes .../public/favicon/android-chrome-192x192.png | Bin 0 -> 6932 bytes .../public/favicon/android-chrome-512x512.png | Bin 0 -> 31206 bytes dev-wiki/public/favicon/apple-touch-icon.png | Bin 0 -> 6156 bytes dev-wiki/public/favicon/favicon-16x16.png | Bin 0 -> 260 bytes dev-wiki/public/favicon/favicon-32x32.png | Bin 0 -> 477 bytes dev-wiki/public/favicon/favicon.ico | Bin 0 -> 15086 bytes dev-wiki/public/favicon/site.webmanifest | 11 + dev-wiki/public/fonts/inter/bold-italic.ttf | Bin 0 -> 348180 bytes dev-wiki/public/fonts/inter/bold.ttf | Bin 0 -> 344152 bytes dev-wiki/public/fonts/inter/heavy-italic.ttf | Bin 0 -> 348712 bytes dev-wiki/public/fonts/inter/heavy.ttf | Bin 0 -> 344820 bytes dev-wiki/public/fonts/inter/light-italic.ttf | Bin 0 -> 347316 bytes dev-wiki/public/fonts/inter/light.ttf | Bin 0 -> 343704 bytes dev-wiki/public/fonts/inter/medium-italic.ttf | Bin 0 -> 346884 bytes dev-wiki/public/fonts/inter/medium.ttf | Bin 0 -> 343200 bytes .../public/fonts/inter/regular-italic.ttf | Bin 0 -> 346480 bytes dev-wiki/public/fonts/inter/regular.ttf | Bin 0 -> 342680 bytes .../public/fonts/inter/semibold-italic.ttf | Bin 0 -> 347760 bytes dev-wiki/public/fonts/inter/semibold.ttf | Bin 0 -> 343828 bytes dev-wiki/public/fonts/inter/thin-italic.ttf | Bin 0 -> 346916 bytes dev-wiki/public/fonts/inter/thin.ttf | Bin 0 -> 343088 bytes .../public/fonts/inter/ultrabold-italic.ttf | Bin 0 -> 349064 bytes dev-wiki/public/fonts/inter/ultrabold.ttf | Bin 0 -> 345008 bytes .../public/fonts/inter/ultralight-italic.ttf | Bin 0 -> 347452 bytes dev-wiki/public/fonts/inter/ultralight.ttf | Bin 0 -> 343532 bytes dev-wiki/public/icons/icon-128x128.png | Bin 0 -> 6992 bytes dev-wiki/public/icons/icon-180x180.png | Bin 0 -> 8088 bytes dev-wiki/public/icons/icon-192x192.png | Bin 0 -> 8605 bytes dev-wiki/public/icons/icon-348x348.png | Bin 0 -> 15847 bytes dev-wiki/public/icons/icon-512x512.png | Bin 0 -> 25169 bytes dev-wiki/public/images/logo-spinner-dark.gif | Bin 0 -> 36427 bytes dev-wiki/public/images/logo-spinner-light.gif | Bin 0 -> 35819 bytes dev-wiki/public/instance-not-ready.webp | Bin 0 -> 23952 bytes dev-wiki/public/instance-setup-done.webp | Bin 0 -> 90384 bytes dev-wiki/public/logos/github-black.png | Bin 0 -> 14032 bytes dev-wiki/public/logos/github-dark.svg | 3 + dev-wiki/public/logos/github-square.png | Bin 0 -> 2352 bytes dev-wiki/public/logos/github-white.png | Bin 0 -> 16559 bytes dev-wiki/public/logos/gitlab-logo.svg | 1 + dev-wiki/public/logos/google-logo.svg | 1 + dev-wiki/public/logos/pi.png | Bin 0 -> 398 bytes dev-wiki/public/mac-command.svg | 2 + dev-wiki/public/maintenance-mode.webp | Bin 0 -> 119864 bytes dev-wiki/public/manifest.json | 27 + dev-wiki/public/marketplace/connect.svg | 3 + dev-wiki/public/og-image.png | Bin 0 -> 158143 bytes .../create-join-workspace-dark.webp | Bin 0 -> 27102 bytes .../create-join-workspace-light.webp | Bin 0 -> 34658 bytes dev-wiki/public/onboarding/cycles.webp | Bin 0 -> 154204 bytes .../onboarding/invite-members-dark.webp | Bin 0 -> 20190 bytes .../onboarding/invite-members-light.webp | Bin 0 -> 21910 bytes dev-wiki/public/onboarding/issues.webp | Bin 0 -> 202198 bytes dev-wiki/public/onboarding/modules.webp | Bin 0 -> 163952 bytes .../public/onboarding/onboarding-pages.webp | Bin 0 -> 45508 bytes dev-wiki/public/onboarding/pages.webp | Bin 0 -> 226948 bytes .../public/onboarding/profile-setup-dark.webp | Bin 0 -> 43108 bytes .../onboarding/profile-setup-light.webp | Bin 0 -> 56108 bytes .../onboarding/user-personalization-dark.webp | Bin 0 -> 28146 bytes .../user-personalization-light.webp | Bin 0 -> 30398 bytes dev-wiki/public/onboarding/views.webp | Bin 0 -> 134924 bytes .../black-horizontal-with-blue-logo.png | Bin 0 -> 2852 bytes .../public/plane-logos/blue-without-text.png | Bin 0 -> 2460 bytes .../plane-logos/plane-business-logo.svg | 12 + .../public/plane-logos/plane-mobile-pwa.png | Bin 0 -> 73863 bytes .../public/plane-logos/plane-one-silver.svg | 19 + dev-wiki/public/plane-logos/plane-one.svg | 7 + .../plane-logos/plane-pro-without-text.svg | 5 + dev-wiki/public/plane-logos/plane-pro.svg | 8 + .../white-horizontal-with-blue-logo.png | Bin 0 -> 2412 bytes .../public/plane-logos/white-horizontal.svg | 17 + dev-wiki/public/plane-takeoff.png | Bin 0 -> 47818 bytes dev-wiki/public/projects/dark-upgrade.svg | 36 + dev-wiki/public/projects/light-upgrade.svg | 42 + .../public/projects/project-states-dark.webp | Bin 0 -> 53996 bytes .../public/projects/project-states-light.webp | Bin 0 -> 47518 bytes dev-wiki/public/projects/states-dark.svg | 46 + dev-wiki/public/projects/states-light.svg | 46 + dev-wiki/public/services/asana.svg | 1 + dev-wiki/public/services/csv.svg | 12 + dev-wiki/public/services/excel.svg | 11 + dev-wiki/public/services/flatfile.svg | 1759 +++++++++++++++++ dev-wiki/public/services/github-dark.svg | 14 + dev-wiki/public/services/github-light.svg | 14 + dev-wiki/public/services/github.png | Bin 0 -> 2352 bytes dev-wiki/public/services/github.svg | 15 + dev-wiki/public/services/gitlab.svg | 1 + dev-wiki/public/services/jira.svg | 15 + dev-wiki/public/services/json.svg | 12 + dev-wiki/public/services/linear.svg | 1 + dev-wiki/public/services/slack.png | Bin 0 -> 19970 bytes dev-wiki/public/site.webmanifest.json | 13 + dev-wiki/public/sw.js | 1 + .../upcoming-features/imports-cta-1-dark.png | Bin 0 -> 44084 bytes .../upcoming-features/imports-cta-1-light.png | Bin 0 -> 42864 bytes .../upcoming-features/imports-cta-2-dark.png | Bin 0 -> 9887 bytes .../upcoming-features/imports-cta-2-light.png | Bin 0 -> 77522 bytes .../integrations-cta-1-dark.png | Bin 0 -> 28340 bytes .../integrations-cta-1-light.png | Bin 0 -> 29029 bytes .../integrations-cta-2-dark.png | Bin 0 -> 59721 bytes .../integrations-cta-2-light.png | Bin 0 -> 68848 bytes dev-wiki/public/user.png | Bin 0 -> 99267 bytes dev-wiki/public/users/user-1.png | Bin 0 -> 4193 bytes dev-wiki/public/users/user-2.png | Bin 0 -> 3781 bytes .../users/user-profile-cover-default-img.png | Bin 0 -> 76661 bytes dev-wiki/public/users/user.png | Bin 0 -> 1419 bytes dev-wiki/public/workbox-91d218dd.js | 1 + .../workspace-active-cycles/cta-l-1-dark.webp | Bin 0 -> 5628 bytes .../cta-l-1-light.webp | Bin 0 -> 28716 bytes .../workspace-active-cycles/cta-r-1-dark.webp | Bin 0 -> 59460 bytes .../cta-r-1-light.webp | Bin 0 -> 64318 bytes .../workspace-active-cycles/cta-r-2-dark.webp | Bin 0 -> 207310 bytes .../cta-r-2-light.webp | Bin 0 -> 392122 bytes .../workspace/workspace-creation-disabled.png | Bin 0 -> 191452 bytes .../workspace/workspace-not-available.png | Bin 0 -> 192990 bytes dev-wiki/styles/command-pallette.css | 51 + dev-wiki/styles/emoji.css | 52 + dev-wiki/styles/globals.css | 1335 +++++++++++++ dev-wiki/styles/nprogress.css | 39 + dev-wiki/styles/react-day-picker.css | 311 +++ dev-wiki/tailwind.config.js | 7 + dev-wiki/tsconfig.json | 24 + dev-wiki/use-font-face-observer.d.ts | 1 + package.json | 3 +- 915 files changed, 36100 insertions(+), 1 deletion(-) create mode 100644 dev-wiki/.env.example create mode 100644 dev-wiki/.eslintignore create mode 100644 dev-wiki/.eslintrc.js create mode 100644 dev-wiki/.prettierignore create mode 100644 dev-wiki/.prettierrc create mode 100644 dev-wiki/.yarnrc.yml create mode 100644 dev-wiki/Dockerfile.dev create mode 100644 dev-wiki/Dockerfile.web create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/archived/layout.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/archived/page.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/filters-header.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/header.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/private/layout.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/private/page.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/public/layout.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/public/page.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/header.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/layout.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/page.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/header.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/layout.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/page.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/sidebar.tsx create mode 100644 dev-wiki/app/(all)/[workspaceSlug]/layout.tsx create mode 100644 dev-wiki/app/(all)/layout.preload.tsx create mode 100644 dev-wiki/app/(all)/layout.tsx create mode 100644 dev-wiki/app/error.tsx create mode 100644 dev-wiki/app/global-error.tsx create mode 100644 dev-wiki/app/layout.tsx create mode 100644 dev-wiki/app/not-found.tsx create mode 100644 dev-wiki/app/provider.tsx create mode 100644 dev-wiki/ce/components/pages/extra-actions.tsx create mode 100644 dev-wiki/ce/layouts/workspace-wrapper.tsx create mode 100644 dev-wiki/core/components/auth-screens/index.ts create mode 100644 dev-wiki/core/components/auth-screens/not-authorized-view.tsx create mode 100644 dev-wiki/core/components/auth-screens/workspace/index.ts create mode 100644 dev-wiki/core/components/auth-screens/workspace/not-a-member.tsx create mode 100644 dev-wiki/core/components/common/access-field.tsx create mode 100644 dev-wiki/core/components/common/activity/activity-block.tsx create mode 100644 dev-wiki/core/components/common/activity/activity-item.tsx create mode 100644 dev-wiki/core/components/common/activity/helper.tsx create mode 100644 dev-wiki/core/components/common/activity/index.ts create mode 100644 dev-wiki/core/components/common/activity/user.tsx create mode 100644 dev-wiki/core/components/common/applied-filters/date.tsx create mode 100644 dev-wiki/core/components/common/applied-filters/index.ts create mode 100644 dev-wiki/core/components/common/applied-filters/members.tsx create mode 100644 dev-wiki/core/components/common/breadcrumb-link.tsx create mode 100644 dev-wiki/core/components/common/count-chip.tsx create mode 100644 dev-wiki/core/components/common/custom-error.tsx create mode 100644 dev-wiki/core/components/common/empty-state.tsx create mode 100644 dev-wiki/core/components/common/filters/created-at.tsx create mode 100644 dev-wiki/core/components/common/filters/created-by.tsx create mode 100644 dev-wiki/core/components/common/filters/index.ts create mode 100644 dev-wiki/core/components/common/index.ts create mode 100644 dev-wiki/core/components/common/latest-feature-block.tsx create mode 100644 dev-wiki/core/components/common/logo-spinner.tsx create mode 100644 dev-wiki/core/components/common/logo.tsx create mode 100644 dev-wiki/core/components/common/new-empty-state.tsx create mode 100644 dev-wiki/core/components/common/page-access-icon.tsx create mode 100644 dev-wiki/core/components/common/pro-icon.tsx create mode 100644 dev-wiki/core/components/common/switcher-label.tsx create mode 100644 dev-wiki/core/components/core/app-header.tsx create mode 100644 dev-wiki/core/components/core/content-overflow-HOC.tsx create mode 100644 dev-wiki/core/components/core/content-wrapper.tsx create mode 100644 dev-wiki/core/components/core/filters/date-filter-modal.tsx create mode 100644 dev-wiki/core/components/core/filters/date-filter-select.tsx create mode 100644 dev-wiki/core/components/core/filters/index.ts create mode 100644 dev-wiki/core/components/core/image-picker-popover.tsx create mode 100644 dev-wiki/core/components/core/index.ts create mode 100644 dev-wiki/core/components/core/list/index.ts create mode 100644 dev-wiki/core/components/core/list/list-item.tsx create mode 100644 dev-wiki/core/components/core/list/list-root.tsx create mode 100644 dev-wiki/core/components/core/page-title.tsx create mode 100644 dev-wiki/core/components/core/render-if-visible-HOC.tsx create mode 100644 dev-wiki/core/components/core/sidebar/index.ts create mode 100644 dev-wiki/core/components/core/sidebar/progress-chart.tsx create mode 100644 dev-wiki/core/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx create mode 100644 dev-wiki/core/components/core/sidebar/single-progress-stats.tsx create mode 100644 dev-wiki/core/components/core/theme/color-picker-input.tsx create mode 100644 dev-wiki/core/components/core/theme/custom-theme-selector.tsx create mode 100644 dev-wiki/core/components/core/theme/index.ts create mode 100644 dev-wiki/core/components/core/theme/theme-switch.tsx create mode 100644 dev-wiki/core/components/editor/embeds/index.ts create mode 100644 dev-wiki/core/components/editor/embeds/mentions/index.ts create mode 100644 dev-wiki/core/components/editor/embeds/mentions/root.tsx create mode 100644 dev-wiki/core/components/editor/embeds/mentions/user.tsx create mode 100644 dev-wiki/core/components/editor/index.ts create mode 100644 dev-wiki/core/components/editor/index.tsx create mode 100644 dev-wiki/core/components/empty-state/index.tsx create mode 100644 dev-wiki/core/components/instance/index.ts create mode 100644 dev-wiki/core/components/instance/maintenance-view.tsx create mode 100644 dev-wiki/core/components/instance/not-ready-view.tsx create mode 100644 dev-wiki/core/components/issues/issue-layouts/properties/all-properties.tsx create mode 100644 dev-wiki/core/components/issues/issue-layouts/utils.ts create mode 100644 dev-wiki/core/components/pages/dropdowns/actions.tsx create mode 100644 dev-wiki/core/components/pages/dropdowns/index.ts create mode 100644 dev-wiki/core/components/pages/editor/breadcrumb-page-item.tsx create mode 100644 dev-wiki/core/components/pages/editor/editor-body.tsx create mode 100644 dev-wiki/core/components/pages/editor/header/index.ts create mode 100644 dev-wiki/core/components/pages/editor/header/logo-picker.tsx create mode 100644 dev-wiki/core/components/pages/editor/header/root.tsx create mode 100644 dev-wiki/core/components/pages/editor/index.ts create mode 100644 dev-wiki/core/components/pages/editor/page-root.tsx create mode 100644 dev-wiki/core/components/pages/editor/summary/content-browser.tsx create mode 100644 dev-wiki/core/components/pages/editor/summary/heading-components.tsx create mode 100644 dev-wiki/core/components/pages/editor/summary/index.ts create mode 100644 dev-wiki/core/components/pages/editor/title.tsx create mode 100644 dev-wiki/core/components/pages/editor/toolbar/color-dropdown.tsx create mode 100644 dev-wiki/core/components/pages/editor/toolbar/index.ts create mode 100644 dev-wiki/core/components/pages/editor/toolbar/info-popover.tsx create mode 100644 dev-wiki/core/components/pages/editor/toolbar/options-dropdown.tsx create mode 100644 dev-wiki/core/components/pages/editor/toolbar/root.tsx create mode 100644 dev-wiki/core/components/pages/editor/toolbar/toolbar.tsx create mode 100644 dev-wiki/core/components/pages/header/actions.tsx create mode 100644 dev-wiki/core/components/pages/header/archived-badge.tsx create mode 100644 dev-wiki/core/components/pages/header/copy-link-control.tsx create mode 100644 dev-wiki/core/components/pages/header/favorite-control.tsx create mode 100644 dev-wiki/core/components/pages/header/offline-badge.tsx create mode 100644 dev-wiki/core/components/pages/header/syncing-badge.tsx create mode 100644 dev-wiki/core/components/pages/index.ts create mode 100644 dev-wiki/core/components/pages/list/applied-filters/index.ts create mode 100644 dev-wiki/core/components/pages/list/applied-filters/root.tsx create mode 100644 dev-wiki/core/components/pages/list/block-item-action.tsx create mode 100644 dev-wiki/core/components/pages/list/block-root.tsx create mode 100644 dev-wiki/core/components/pages/list/block.tsx create mode 100644 dev-wiki/core/components/pages/list/filters/index.ts create mode 100644 dev-wiki/core/components/pages/list/filters/root.tsx create mode 100644 dev-wiki/core/components/pages/list/index.ts create mode 100644 dev-wiki/core/components/pages/list/order-by.tsx create mode 100644 dev-wiki/core/components/pages/list/search-input.tsx create mode 100644 dev-wiki/core/components/pages/list/tab-navigation.tsx create mode 100644 dev-wiki/core/components/pages/loaders/index.ts create mode 100644 dev-wiki/core/components/pages/loaders/page-content-loader.tsx create mode 100644 dev-wiki/core/components/pages/loaders/page-loader.tsx create mode 100644 dev-wiki/core/components/pages/modals/create-page-modal.tsx create mode 100644 dev-wiki/core/components/pages/modals/delete-page-modal.tsx create mode 100644 dev-wiki/core/components/pages/modals/index.ts create mode 100644 dev-wiki/core/components/pages/modals/page-form.tsx create mode 100644 dev-wiki/core/components/pages/version/editor.tsx create mode 100644 dev-wiki/core/components/pages/version/index.ts create mode 100644 dev-wiki/core/components/pages/version/main-content.tsx create mode 100644 dev-wiki/core/components/pages/version/root.tsx create mode 100644 dev-wiki/core/components/pages/version/sidebar-list-item.tsx create mode 100644 dev-wiki/core/components/pages/version/sidebar-list.tsx create mode 100644 dev-wiki/core/components/pages/version/sidebar-root.tsx create mode 100644 dev-wiki/core/components/sidebar/index.ts create mode 100644 dev-wiki/core/components/sidebar/sidebar-navigation.tsx create mode 100644 dev-wiki/core/components/ui/index.ts create mode 100644 dev-wiki/core/components/ui/view-list-loader.tsx create mode 100644 dev-wiki/core/components/user/index.ts create mode 100644 dev-wiki/core/components/user/user-greetings.tsx create mode 100644 dev-wiki/core/components/workspace/billing/index.ts create mode 100644 dev-wiki/core/components/workspace/billing/subscription.ts create mode 100644 dev-wiki/core/components/workspace/index.ts create mode 100644 dev-wiki/core/components/workspace/logo.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/dropdown-item.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/dropdown.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-folder.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-drag-handle.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-quick-action.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-title.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-items/common/favorite-item-wrapper.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-items/common/helper.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-items/common/index.ts create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-items/index.ts create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorite-items/root.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorites-menu.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/favorites.helpers.ts create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/index.ts create mode 100644 dev-wiki/core/components/workspace/sidebar/favorites/new-fav-folder.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/index.ts create mode 100644 dev-wiki/core/components/workspace/sidebar/workspace-menu-header.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/workspace-menu-item.tsx create mode 100644 dev-wiki/core/components/workspace/sidebar/workspace-menu.tsx create mode 100644 dev-wiki/core/constants/editor.ts create mode 100644 dev-wiki/core/hooks/editor/index.ts create mode 100644 dev-wiki/core/hooks/editor/use-editor-config.ts create mode 100644 dev-wiki/core/hooks/editor/use-editor-mention.tsx create mode 100644 dev-wiki/core/hooks/store/index.ts create mode 100644 dev-wiki/core/hooks/store/use-app-theme.ts create mode 100644 dev-wiki/core/hooks/store/use-editor-asset.ts create mode 100644 dev-wiki/core/hooks/store/use-favorite.ts create mode 100644 dev-wiki/core/hooks/store/use-instance.ts create mode 100644 dev-wiki/core/hooks/store/use-member.ts create mode 100644 dev-wiki/core/hooks/store/use-router-params.ts create mode 100644 dev-wiki/core/hooks/store/use-workspace.ts create mode 100644 dev-wiki/core/hooks/store/user/index.ts create mode 100644 dev-wiki/core/hooks/store/user/user-permissions.ts create mode 100644 dev-wiki/core/hooks/store/user/user-user-profile.ts create mode 100644 dev-wiki/core/hooks/store/user/user-user-settings.ts create mode 100644 dev-wiki/core/hooks/store/user/user-user.ts create mode 100644 dev-wiki/core/hooks/use-app-router.tsx create mode 100644 dev-wiki/core/hooks/use-auto-save.tsx create mode 100644 dev-wiki/core/hooks/use-collaborative-page-actions.tsx create mode 100644 dev-wiki/core/hooks/use-current-time.tsx create mode 100644 dev-wiki/core/hooks/use-dropdown-key-down.tsx create mode 100644 dev-wiki/core/hooks/use-keypress.tsx create mode 100644 dev-wiki/core/hooks/use-local-storage.tsx create mode 100644 dev-wiki/core/hooks/use-online-status.ts create mode 100644 dev-wiki/core/hooks/use-page-fallback.ts create mode 100644 dev-wiki/core/hooks/use-page-filters.ts create mode 100644 dev-wiki/core/hooks/use-page-operations.tsx create mode 100644 dev-wiki/core/hooks/use-platform-os.tsx create mode 100644 dev-wiki/core/hooks/use-query-params.ts create mode 100644 dev-wiki/core/hooks/use-realtime-page-events.tsx create mode 100644 dev-wiki/core/hooks/use-reload-confirmation.tsx create mode 100644 dev-wiki/core/hooks/use-resolved-asset-path.tsx create mode 100644 dev-wiki/core/layouts/access/index.ts create mode 100644 dev-wiki/core/layouts/access/workspace-wrapper.tsx create mode 100644 dev-wiki/core/layouts/auth-layout/index.ts create mode 100644 dev-wiki/core/layouts/auth-layout/workspace-wrapper.tsx create mode 100644 dev-wiki/core/layouts/default-layout/index.tsx create mode 100644 dev-wiki/core/lib/n-progress/AppProgressBar.tsx create mode 100644 dev-wiki/core/lib/n-progress/index.tsx create mode 100644 dev-wiki/core/lib/n-progress/utils/getAnchorProperty.ts create mode 100644 dev-wiki/core/lib/n-progress/utils/sameURL.ts create mode 100644 dev-wiki/core/lib/n-progress/withSuspense.tsx create mode 100644 dev-wiki/core/lib/polyfills/index.ts create mode 100644 dev-wiki/core/lib/polyfills/requestIdleCallback.ts create mode 100644 dev-wiki/core/lib/store-context.tsx create mode 100644 dev-wiki/core/lib/wrappers/authentication-wrapper.tsx create mode 100644 dev-wiki/core/lib/wrappers/index.ts create mode 100644 dev-wiki/core/lib/wrappers/instance-wrapper.tsx create mode 100644 dev-wiki/core/lib/wrappers/store-wrapper.tsx create mode 100644 dev-wiki/core/services/ai.service.ts create mode 100644 dev-wiki/core/services/api.service.ts create mode 100644 dev-wiki/core/services/api_token.service.ts create mode 100644 dev-wiki/core/services/auth.service.ts create mode 100644 dev-wiki/core/services/favorite/favorite.service.ts create mode 100644 dev-wiki/core/services/favorite/index.ts create mode 100644 dev-wiki/core/services/file-upload.service.ts create mode 100644 dev-wiki/core/services/file.service.ts create mode 100644 dev-wiki/core/services/instance.service.ts create mode 100644 dev-wiki/core/services/user.service.ts create mode 100644 dev-wiki/core/services/webhook.service.ts create mode 100644 dev-wiki/core/services/workspace.service.ts create mode 100644 dev-wiki/core/store/editor/asset.store.ts create mode 100644 dev-wiki/core/store/favorite.store.ts create mode 100644 dev-wiki/core/store/instance.store.ts create mode 100644 dev-wiki/core/store/member/index.ts create mode 100644 dev-wiki/core/store/member/workspace-member.store.ts create mode 100644 dev-wiki/core/store/pages/base-page.ts create mode 100644 dev-wiki/core/store/pages/project-page.store.ts create mode 100644 dev-wiki/core/store/root.store.ts create mode 100644 dev-wiki/core/store/router.store.ts create mode 100644 dev-wiki/core/store/theme.store.ts create mode 100644 dev-wiki/core/store/user/account.store.ts create mode 100644 dev-wiki/core/store/user/base-permissions.store.ts create mode 100644 dev-wiki/core/store/user/index.ts create mode 100644 dev-wiki/core/store/user/profile.store.ts create mode 100644 dev-wiki/core/store/user/settings.store.ts create mode 100644 dev-wiki/core/store/workspace/api-token.store.ts create mode 100644 dev-wiki/core/store/workspace/home.ts create mode 100644 dev-wiki/core/store/workspace/index.ts create mode 100644 dev-wiki/core/store/workspace/link.store.ts create mode 100644 dev-wiki/core/store/workspace/webhook.store.ts create mode 100644 dev-wiki/ee/components/feature-flags/index.tsx create mode 100644 dev-wiki/ee/components/feature-flags/with-feature-flag-hoc.tsx create mode 100644 dev-wiki/ee/components/global/index.ts create mode 100644 dev-wiki/ee/components/global/index.tsx create mode 100644 dev-wiki/ee/components/global/version-number.tsx create mode 100644 dev-wiki/ee/components/instance/index.ts create mode 100644 dev-wiki/ee/components/instance/maintenance-message.tsx create mode 100644 dev-wiki/ee/components/issues/index.tsx create mode 100644 dev-wiki/ee/components/pages/badge/archived-badge.tsx create mode 100644 dev-wiki/ee/components/pages/badge/badge.tsx create mode 100644 dev-wiki/ee/components/pages/badge/index.ts create mode 100644 dev-wiki/ee/components/pages/badge/moved-badge.tsx create mode 100644 dev-wiki/ee/components/pages/collaborators-list.tsx create mode 100644 dev-wiki/ee/components/pages/editor/ai/ask-pi-menu.tsx create mode 100644 dev-wiki/ee/components/pages/editor/ai/index.ts create mode 100644 dev-wiki/ee/components/pages/editor/ai/menu.tsx create mode 100644 dev-wiki/ee/components/pages/editor/embed/index.ts create mode 100644 dev-wiki/ee/components/pages/editor/embed/issue-embed-upgrade-card.tsx create mode 100644 dev-wiki/ee/components/pages/editor/embed/issue-embed.tsx create mode 100644 dev-wiki/ee/components/pages/editor/index.ts create mode 100644 dev-wiki/ee/components/pages/embed/content.tsx create mode 100644 dev-wiki/ee/components/pages/embed/index.ts create mode 100644 dev-wiki/ee/components/pages/embed/preview.tsx create mode 100644 dev-wiki/ee/components/pages/embed/root.tsx create mode 100644 dev-wiki/ee/components/pages/extra-actions.tsx create mode 100644 dev-wiki/ee/components/pages/header/collaborators-list.tsx create mode 100644 dev-wiki/ee/components/pages/header/lock-control.tsx create mode 100644 dev-wiki/ee/components/pages/index.ts create mode 100644 dev-wiki/ee/components/pages/list/block-item-action.tsx create mode 100644 dev-wiki/ee/components/pages/list/block.tsx create mode 100644 dev-wiki/ee/components/pages/list/index.ts create mode 100644 dev-wiki/ee/components/pages/list/root.tsx create mode 100644 dev-wiki/ee/components/pages/modals/confirmation-modal.tsx create mode 100644 dev-wiki/ee/components/pages/modals/create-page-modal.tsx create mode 100644 dev-wiki/ee/components/pages/modals/delete-page-modal.tsx create mode 100644 dev-wiki/ee/components/pages/modals/index.ts create mode 100644 dev-wiki/ee/components/pages/modals/lock-page-modal.tsx create mode 100644 dev-wiki/ee/components/pages/modals/multiple-page-delete-modal.tsx create mode 100644 dev-wiki/ee/components/pages/modals/publish-page-modal.tsx create mode 100644 dev-wiki/ee/components/pages/quick-actions.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/index.ts create mode 100644 dev-wiki/ee/components/pages/sidebar/list-item-root.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/list-item.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/list.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/menu.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/quick-actions.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/section/components/index.ts create mode 100644 dev-wiki/ee/components/pages/sidebar/section/components/section-content.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/section/components/section-header.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/section/constants.ts create mode 100644 dev-wiki/ee/components/pages/sidebar/section/hooks/index.ts create mode 100644 dev-wiki/ee/components/pages/sidebar/section/hooks/use-section-drag-drop.ts create mode 100644 dev-wiki/ee/components/pages/sidebar/section/hooks/use-section-pages.ts create mode 100644 dev-wiki/ee/components/pages/sidebar/section/index.ts create mode 100644 dev-wiki/ee/components/pages/sidebar/section/section-root.tsx create mode 100644 dev-wiki/ee/components/pages/sidebar/section/types.ts create mode 100644 dev-wiki/ee/components/pages/version/editor.tsx create mode 100644 dev-wiki/ee/components/pages/version/index.ts create mode 100644 dev-wiki/ee/components/pages/workspace-pages-upgrade.tsx create mode 100644 dev-wiki/ee/components/sidebar/app-switcher.tsx create mode 100644 dev-wiki/ee/components/sidebar/index.ts create mode 100644 dev-wiki/ee/components/workspace-disabled.tsx create mode 100644 dev-wiki/ee/components/workspace/edition-badge.tsx create mode 100644 dev-wiki/ee/components/workspace/index.ts create mode 100644 dev-wiki/ee/components/workspace/index.tsx create mode 100644 dev-wiki/ee/components/workspace/sidebar/index.tsx create mode 100644 dev-wiki/ee/constants/ai.ts create mode 100644 dev-wiki/ee/constants/editor.ts create mode 100644 dev-wiki/ee/constants/index.ts create mode 100644 dev-wiki/ee/constants/sidebar-favorites.ts create mode 100644 dev-wiki/ee/helpers/dashboard.helper.ts create mode 100644 dev-wiki/ee/helpers/instance.helper.ts create mode 100644 dev-wiki/ee/hooks/context/use-project-creation.ts create mode 100644 dev-wiki/ee/hooks/store/index.ts create mode 100644 dev-wiki/ee/hooks/store/use-feature-flags.ts create mode 100644 dev-wiki/ee/hooks/store/use-flag.ts create mode 100644 dev-wiki/ee/hooks/store/use-page-store.ts create mode 100644 dev-wiki/ee/hooks/store/use-page.ts create mode 100644 dev-wiki/ee/hooks/store/use-publish-page.ts create mode 100644 dev-wiki/ee/hooks/store/use-self-hosted-subscription.ts create mode 100644 dev-wiki/ee/hooks/store/use-workspace-features.ts create mode 100644 dev-wiki/ee/hooks/use-additional-editor-mention.tsx create mode 100644 dev-wiki/ee/hooks/use-additional-favorite-item-details.ts create mode 100644 dev-wiki/ee/hooks/use-editor-embed.tsx create mode 100644 dev-wiki/ee/hooks/use-editor-flagging.ts create mode 100644 dev-wiki/ee/hooks/use-file-size.ts create mode 100644 dev-wiki/ee/hooks/use-issue-embed.tsx create mode 100644 dev-wiki/ee/hooks/use-page-actions-menu.tsx create mode 100644 dev-wiki/ee/hooks/use-page-flag.ts create mode 100644 dev-wiki/ee/hooks/use-workspace-subscription.ts create mode 100644 dev-wiki/ee/layouts/workspace-wrapper.tsx create mode 100644 dev-wiki/ee/services/feature-flag.service.ts create mode 100644 dev-wiki/ee/services/index.ts create mode 100644 dev-wiki/ee/services/page/index.ts create mode 100644 dev-wiki/ee/services/page/publish-page.service.ts create mode 100644 dev-wiki/ee/services/page/workspace-page-version.service.ts create mode 100644 dev-wiki/ee/services/page/workspace-page.service.ts create mode 100644 dev-wiki/ee/services/payment.service.ts create mode 100644 dev-wiki/ee/services/self-hosted-subscription.service.ts create mode 100644 dev-wiki/ee/services/workspace-feature.service.ts create mode 100644 dev-wiki/ee/services/workspace.service.ts create mode 100644 dev-wiki/ee/store/feature-flags/feature-flags.store.ts create mode 100644 dev-wiki/ee/store/pages/publish-page.store.ts create mode 100644 dev-wiki/ee/store/pages/workspace-page.store.ts create mode 100644 dev-wiki/ee/store/pages/workspace-page.ts create mode 100644 dev-wiki/ee/store/root.store.ts create mode 100644 dev-wiki/ee/store/subscription/self-hosted-subscription.store.ts create mode 100644 dev-wiki/ee/store/subscription/subscription.store.ts create mode 100644 dev-wiki/ee/store/user/permission.store.ts create mode 100644 dev-wiki/ee/store/workspace-feature.store.ts create mode 100644 dev-wiki/ee/types/app.d.ts create mode 100644 dev-wiki/ee/types/index.ts create mode 100644 dev-wiki/ee/types/pages.d.ts create mode 100644 dev-wiki/ee/types/self-hosted-subscription.ts create mode 100644 dev-wiki/ee/types/workspace-feature.ts create mode 100644 dev-wiki/google.d.ts create mode 100644 dev-wiki/helpers/array.helper.ts create mode 100644 dev-wiki/helpers/authentication.helper.tsx create mode 100644 dev-wiki/helpers/color.helper.ts create mode 100644 dev-wiki/helpers/common.helper.ts create mode 100644 dev-wiki/helpers/date-time.helper.ts create mode 100644 dev-wiki/helpers/editor.helper.ts create mode 100644 dev-wiki/helpers/emoji.helper.tsx create mode 100644 dev-wiki/helpers/file.helper.ts create mode 100644 dev-wiki/helpers/filter.helper.ts create mode 100644 dev-wiki/helpers/page.helper.ts create mode 100644 dev-wiki/helpers/string.helper.ts create mode 100644 dev-wiki/helpers/tab-indices.helper.ts create mode 100644 dev-wiki/helpers/theme.helper.ts create mode 100644 dev-wiki/helpers/user.helper.ts create mode 100644 dev-wiki/manifest.json create mode 100644 dev-wiki/next-env.d.ts create mode 100644 dev-wiki/next.config.js create mode 100644 dev-wiki/null create mode 100644 dev-wiki/package.json create mode 100644 dev-wiki/postcss.config.js create mode 100644 dev-wiki/public/.well-known/apple-app-site-association create mode 100644 dev-wiki/public/.well-known/assetlinks.json create mode 100644 dev-wiki/public/404.svg create mode 100644 dev-wiki/public/animated-icons/uploading.json create mode 100644 dev-wiki/public/attachment/audio-icon.png create mode 100644 dev-wiki/public/attachment/css-icon.png create mode 100644 dev-wiki/public/attachment/csv-icon.png create mode 100644 dev-wiki/public/attachment/default-icon.png create mode 100644 dev-wiki/public/attachment/doc-icon.png create mode 100644 dev-wiki/public/attachment/excel-icon.png create mode 100644 dev-wiki/public/attachment/figma-icon.png create mode 100644 dev-wiki/public/attachment/html-icon.png create mode 100644 dev-wiki/public/attachment/img-icon.png create mode 100644 dev-wiki/public/attachment/jpg-icon.png create mode 100644 dev-wiki/public/attachment/js-icon.png create mode 100644 dev-wiki/public/attachment/pdf-icon.png create mode 100644 dev-wiki/public/attachment/png-icon.png create mode 100644 dev-wiki/public/attachment/rar-icon.png create mode 100644 dev-wiki/public/attachment/svg-icon.png create mode 100644 dev-wiki/public/attachment/txt-icon.png create mode 100644 dev-wiki/public/attachment/video-icon.png create mode 100644 dev-wiki/public/attachment/zip-icon.png create mode 100644 dev-wiki/public/auth/access-denied.svg create mode 100644 dev-wiki/public/auth/background-pattern-dark.svg create mode 100644 dev-wiki/public/auth/background-pattern.svg create mode 100644 dev-wiki/public/auth/project-not-authorized.svg create mode 100644 dev-wiki/public/auth/unauthorized.svg create mode 100644 dev-wiki/public/auth/workspace-not-authorized.svg create mode 100644 dev-wiki/public/emoji/project-emoji.svg create mode 100644 dev-wiki/public/empty-state/active-cycle/assignee-dark.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/assignee-light.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/chart-dark.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/chart-light.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/cycle-dark.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/cycle-light.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/label-dark.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/label-light.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/priority-dark.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/priority-light.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/progress-dark.webp create mode 100644 dev-wiki/public/empty-state/active-cycle/progress-light.webp create mode 100644 dev-wiki/public/empty-state/all-issues/all-issues-dark.webp create mode 100644 dev-wiki/public/empty-state/all-issues/all-issues-light.webp create mode 100644 dev-wiki/public/empty-state/all-issues/assigned-dark.webp create mode 100644 dev-wiki/public/empty-state/all-issues/assigned-light.webp create mode 100644 dev-wiki/public/empty-state/all-issues/created-dark.webp create mode 100644 dev-wiki/public/empty-state/all-issues/created-light.webp create mode 100644 dev-wiki/public/empty-state/all-issues/custom-view-dark.webp create mode 100644 dev-wiki/public/empty-state/all-issues/custom-view-light.webp create mode 100644 dev-wiki/public/empty-state/all-issues/no-project-dark.webp create mode 100644 dev-wiki/public/empty-state/all-issues/no-project-light.webp create mode 100644 dev-wiki/public/empty-state/all-issues/subscribed-dark.webp create mode 100644 dev-wiki/public/empty-state/all-issues/subscribed-light.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-chart-area-dark.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-chart-area-light.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-chart-bar-dark.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-chart-bar-light.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-chart-radar-dark.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-chart-radar-light.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-grid-background-dark.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-grid-background-light.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-table-dark.webp create mode 100644 dev-wiki/public/empty-state/analytics/empty-table-light.webp create mode 100644 dev-wiki/public/empty-state/api-token.svg create mode 100644 dev-wiki/public/empty-state/archived/empty-cycles-dark.webp create mode 100644 dev-wiki/public/empty-state/archived/empty-cycles-light.webp create mode 100644 dev-wiki/public/empty-state/archived/empty-issues-dark.webp create mode 100644 dev-wiki/public/empty-state/archived/empty-issues-light.webp create mode 100644 dev-wiki/public/empty-state/archived/empty-modules-dark.webp create mode 100644 dev-wiki/public/empty-state/archived/empty-modules-light.webp create mode 100644 dev-wiki/public/empty-state/customers/customer-upgrade-dark.webp create mode 100644 dev-wiki/public/empty-state/customers/customer-upgrade-light.webp create mode 100644 dev-wiki/public/empty-state/customers/customer.svg create mode 100644 dev-wiki/public/empty-state/customers/customers-disabled-dark.webp create mode 100644 dev-wiki/public/empty-state/customers/customers-disabled-light.webp create mode 100644 dev-wiki/public/empty-state/customers/detail-dark.svg create mode 100644 dev-wiki/public/empty-state/customers/detail-light.svg create mode 100644 dev-wiki/public/empty-state/customers/request-search-dark.svg create mode 100644 dev-wiki/public/empty-state/customers/request-search-light.svg create mode 100644 dev-wiki/public/empty-state/customers/search-empty-dark.svg create mode 100644 dev-wiki/public/empty-state/customers/search-empty-light.svg create mode 100644 dev-wiki/public/empty-state/cycle-issues/calendar-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/calendar-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/calendar-light-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/calendar-light.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/gantt_chart-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/gantt_chart-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/gantt_chart-light-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/gantt_chart-light.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/kanban-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/kanban-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/kanban-light-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/kanban-light.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/list-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/list-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/list-light-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/list-light.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/spreadsheet-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/spreadsheet-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/spreadsheet-light-resp.webp create mode 100644 dev-wiki/public/empty-state/cycle-issues/spreadsheet-light.webp create mode 100644 dev-wiki/public/empty-state/cycle.svg create mode 100644 dev-wiki/public/empty-state/cycle/active-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle/active-light.webp create mode 100644 dev-wiki/public/empty-state/cycle/all-filters.svg create mode 100644 dev-wiki/public/empty-state/cycle/completed-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle/completed-light.webp create mode 100644 dev-wiki/public/empty-state/cycle/completed-no-issues-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle/completed-no-issues-light.webp create mode 100644 dev-wiki/public/empty-state/cycle/draft-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle/draft-light.webp create mode 100644 dev-wiki/public/empty-state/cycle/name-filter.svg create mode 100644 dev-wiki/public/empty-state/cycle/upcoming-dark.webp create mode 100644 dev-wiki/public/empty-state/cycle/upcoming-light.webp create mode 100644 dev-wiki/public/empty-state/dashboard/dark/completed-issues.svg create mode 100644 dev-wiki/public/empty-state/dashboard/dark/issues-by-priority.svg create mode 100644 dev-wiki/public/empty-state/dashboard/dark/issues-by-state-group.svg create mode 100644 dev-wiki/public/empty-state/dashboard/dark/overdue-issues.svg create mode 100644 dev-wiki/public/empty-state/dashboard/dark/recent-activity.svg create mode 100644 dev-wiki/public/empty-state/dashboard/dark/recent-collaborators-1.svg create mode 100644 dev-wiki/public/empty-state/dashboard/dark/recent-collaborators-2.svg create mode 100644 dev-wiki/public/empty-state/dashboard/dark/recent-collaborators-3.svg create mode 100644 dev-wiki/public/empty-state/dashboard/dark/upcoming-issues.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/completed-issues.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/issues-by-priority.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/issues-by-state-group.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/overdue-issues.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/recent-activity.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/recent-collaborators-1.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/recent-collaborators-2.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/recent-collaborators-3.svg create mode 100644 dev-wiki/public/empty-state/dashboard/light/upcoming-issues.svg create mode 100644 dev-wiki/public/empty-state/dashboard/widgets-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboard/widgets-light.webp create mode 100644 dev-wiki/public/empty-state/dashboard_empty_project.webp create mode 100644 dev-wiki/public/empty-state/dashboards/feature-flag/inside-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/feature-flag/inside-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/feature-flag/outside-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/feature-flag/outside-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/list-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/list-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/list-search-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/list-search-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/area_chart-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/area_chart-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/bar_chart-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/bar_chart-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/donut_chart-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/donut_chart-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/line_chart-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/line_chart-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/number-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/number-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/pie_chart-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/charts/pie_chart-light.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/list-dark.webp create mode 100644 dev-wiki/public/empty-state/dashboards/widgets/list-light.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/cycles-dark.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/cycles-light.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/intake-dark.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/intake-light.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/modules-dark.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/modules-light.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/pages-dark.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/pages-light.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/views-dark.webp create mode 100644 dev-wiki/public/empty-state/disabled-feature/views-light.webp create mode 100644 dev-wiki/public/empty-state/draft/draft-issues-empty-dark.webp create mode 100644 dev-wiki/public/empty-state/draft/draft-issues-empty-light.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/calendar-dark.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/calendar-light.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/gantt_chart-dark.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/gantt_chart-light.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/kanban-dark.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/kanban-light.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/list-dark.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/list-light.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/spreadsheet-dark.webp create mode 100644 dev-wiki/public/empty-state/empty-filters/spreadsheet-light.webp create mode 100644 dev-wiki/public/empty-state/empty-updates-light.png create mode 100644 dev-wiki/public/empty-state/empty_analytics.webp create mode 100644 dev-wiki/public/empty-state/empty_bar_graph.svg create mode 100644 dev-wiki/public/empty-state/empty_cycles.webp create mode 100644 dev-wiki/public/empty-state/empty_graph.svg create mode 100644 dev-wiki/public/empty-state/empty_issues.webp create mode 100644 dev-wiki/public/empty-state/empty_label.svg create mode 100644 dev-wiki/public/empty-state/empty_members.svg create mode 100644 dev-wiki/public/empty-state/empty_modules.webp create mode 100644 dev-wiki/public/empty-state/empty_page.png create mode 100644 dev-wiki/public/empty-state/empty_project.webp create mode 100644 dev-wiki/public/empty-state/empty_users.svg create mode 100644 dev-wiki/public/empty-state/empty_view.webp create mode 100644 dev-wiki/public/empty-state/epics/epics-dark.webp create mode 100644 dev-wiki/public/empty-state/epics/epics-light.webp create mode 100644 dev-wiki/public/empty-state/epics/settings-dark.png create mode 100644 dev-wiki/public/empty-state/epics/settings-light.png create mode 100644 dev-wiki/public/empty-state/estimates/dark.svg create mode 100644 dev-wiki/public/empty-state/estimates/light.svg create mode 100644 dev-wiki/public/empty-state/initiatives/initiatives-dark.webp create mode 100644 dev-wiki/public/empty-state/initiatives/initiatives-light.webp create mode 100644 dev-wiki/public/empty-state/initiatives/upgrade-dark.webp create mode 100644 dev-wiki/public/empty-state/initiatives/upgrade-light.webp create mode 100644 dev-wiki/public/empty-state/intake/filter-issue-dark.webp create mode 100644 dev-wiki/public/empty-state/intake/filter-issue-light.webp create mode 100644 dev-wiki/public/empty-state/intake/intake-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/intake/intake-dark.webp create mode 100644 dev-wiki/public/empty-state/intake/intake-issue-dark.webp create mode 100644 dev-wiki/public/empty-state/intake/intake-issue-light.webp create mode 100644 dev-wiki/public/empty-state/intake/intake-light-resp.webp create mode 100644 dev-wiki/public/empty-state/intake/intake-light.webp create mode 100644 dev-wiki/public/empty-state/intake/issue-detail-dark.webp create mode 100644 dev-wiki/public/empty-state/intake/issue-detail-light.webp create mode 100644 dev-wiki/public/empty-state/invitation.svg create mode 100644 dev-wiki/public/empty-state/issue-types/issue-type-dark.png create mode 100644 dev-wiki/public/empty-state/issue-types/issue-type-dark.svg create mode 100644 dev-wiki/public/empty-state/issue-types/issue-type-light.png create mode 100644 dev-wiki/public/empty-state/issue-types/issue-type-light.svg create mode 100644 dev-wiki/public/empty-state/issue.svg create mode 100644 dev-wiki/public/empty-state/label.svg create mode 100644 dev-wiki/public/empty-state/marketplace/empty-workspace-dark.png create mode 100644 dev-wiki/public/empty-state/marketplace/empty-workspace-light.png create mode 100644 dev-wiki/public/empty-state/module-issues/calendar-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/calendar-dark.webp create mode 100644 dev-wiki/public/empty-state/module-issues/calendar-light-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/calendar-light.webp create mode 100644 dev-wiki/public/empty-state/module-issues/gantt_chart-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/gantt_chart-dark.webp create mode 100644 dev-wiki/public/empty-state/module-issues/gantt_chart-light-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/gantt_chart-light.webp create mode 100644 dev-wiki/public/empty-state/module-issues/kanban-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/kanban-dark.webp create mode 100644 dev-wiki/public/empty-state/module-issues/kanban-light-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/kanban-light.webp create mode 100644 dev-wiki/public/empty-state/module-issues/list-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/list-dark.webp create mode 100644 dev-wiki/public/empty-state/module-issues/list-light-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/list-light.webp create mode 100644 dev-wiki/public/empty-state/module-issues/spreadsheet-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/spreadsheet-dark.webp create mode 100644 dev-wiki/public/empty-state/module-issues/spreadsheet-light-resp.webp create mode 100644 dev-wiki/public/empty-state/module-issues/spreadsheet-light.webp create mode 100644 dev-wiki/public/empty-state/module.svg create mode 100644 dev-wiki/public/empty-state/module/all-filters.svg create mode 100644 dev-wiki/public/empty-state/module/name-filter.svg create mode 100644 dev-wiki/public/empty-state/notification.svg create mode 100644 dev-wiki/public/empty-state/onboarding/analytics-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/analytics-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/archive-dark.png create mode 100644 dev-wiki/public/empty-state/onboarding/archive-light.png create mode 100644 dev-wiki/public/empty-state/onboarding/cycles-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/cycles-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/dashboard-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/dashboard-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/graph-dark.png create mode 100644 dev-wiki/public/empty-state/onboarding/graph-light.png create mode 100644 dev-wiki/public/empty-state/onboarding/issues-closed-dark.png create mode 100644 dev-wiki/public/empty-state/onboarding/issues-closed-light.png create mode 100644 dev-wiki/public/empty-state/onboarding/issues-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/issues-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/members-dark.png create mode 100644 dev-wiki/public/empty-state/onboarding/members-light.png create mode 100644 dev-wiki/public/empty-state/onboarding/modules-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/modules-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/notification-dark.png create mode 100644 dev-wiki/public/empty-state/onboarding/notification-light.png create mode 100644 dev-wiki/public/empty-state/onboarding/pages-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/pages-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/projects-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/projects-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/search-dark.png create mode 100644 dev-wiki/public/empty-state/onboarding/search-light.png create mode 100644 dev-wiki/public/empty-state/onboarding/snooze-light.png create mode 100644 dev-wiki/public/empty-state/onboarding/snoozed-dark.png create mode 100644 dev-wiki/public/empty-state/onboarding/views-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/views-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/workspace-active-cycles-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/workspace-active-cycles-light.webp create mode 100644 dev-wiki/public/empty-state/onboarding/workspace-invites-dark.webp create mode 100644 dev-wiki/public/empty-state/onboarding/workspace-invites-light.webp create mode 100644 dev-wiki/public/empty-state/pages/all-dark.webp create mode 100644 dev-wiki/public/empty-state/pages/all-filters.svg create mode 100644 dev-wiki/public/empty-state/pages/all-light.webp create mode 100644 dev-wiki/public/empty-state/pages/archived-dark.webp create mode 100644 dev-wiki/public/empty-state/pages/archived-light.webp create mode 100644 dev-wiki/public/empty-state/pages/name-filter.svg create mode 100644 dev-wiki/public/empty-state/pages/private-dark.webp create mode 100644 dev-wiki/public/empty-state/pages/private-light.webp create mode 100644 dev-wiki/public/empty-state/pages/public-dark.webp create mode 100644 dev-wiki/public/empty-state/pages/public-light.webp create mode 100644 dev-wiki/public/empty-state/pi/chat-dark.webp create mode 100644 dev-wiki/public/empty-state/pi/chat-light.webp create mode 100644 dev-wiki/public/empty-state/profile/activities-dark.webp create mode 100644 dev-wiki/public/empty-state/profile/activities-light.webp create mode 100644 dev-wiki/public/empty-state/profile/activity-dark.webp create mode 100644 dev-wiki/public/empty-state/profile/activity-light.webp create mode 100644 dev-wiki/public/empty-state/profile/assigned-dark.webp create mode 100644 dev-wiki/public/empty-state/profile/assigned-light.webp create mode 100644 dev-wiki/public/empty-state/profile/created-dark.webp create mode 100644 dev-wiki/public/empty-state/profile/created-light.webp create mode 100644 dev-wiki/public/empty-state/profile/issues-by-priority-dark.webp create mode 100644 dev-wiki/public/empty-state/profile/issues-by-priority-light.webp create mode 100644 dev-wiki/public/empty-state/profile/issues-by-state-dark.webp create mode 100644 dev-wiki/public/empty-state/profile/issues-by-state-light.webp create mode 100644 dev-wiki/public/empty-state/profile/subscribed-dark.webp create mode 100644 dev-wiki/public/empty-state/profile/subscribed-light.webp create mode 100644 dev-wiki/public/empty-state/project-settings/estimates-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/project-settings/estimates-dark.png create mode 100644 dev-wiki/public/empty-state/project-settings/estimates-dark.webp create mode 100644 dev-wiki/public/empty-state/project-settings/estimates-light-resp.webp create mode 100644 dev-wiki/public/empty-state/project-settings/estimates-light.png create mode 100644 dev-wiki/public/empty-state/project-settings/estimates-light.webp create mode 100644 dev-wiki/public/empty-state/project-settings/integrations-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/project-settings/integrations-dark.webp create mode 100644 dev-wiki/public/empty-state/project-settings/integrations-light-resp.webp create mode 100644 dev-wiki/public/empty-state/project-settings/integrations-light.webp create mode 100644 dev-wiki/public/empty-state/project-settings/labels-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/project-settings/labels-dark.webp create mode 100644 dev-wiki/public/empty-state/project-settings/labels-light-resp.webp create mode 100644 dev-wiki/public/empty-state/project-settings/labels-light.webp create mode 100644 dev-wiki/public/empty-state/project-settings/no-projects-dark.png create mode 100644 dev-wiki/public/empty-state/project-settings/no-projects-light.png create mode 100644 dev-wiki/public/empty-state/project-settings/updates-dark.png create mode 100644 dev-wiki/public/empty-state/project-settings/updates-light.png create mode 100644 dev-wiki/public/empty-state/project.svg create mode 100644 dev-wiki/public/empty-state/project/all-filters.svg create mode 100644 dev-wiki/public/empty-state/project/name-filter.svg create mode 100644 dev-wiki/public/empty-state/recent_activity.svg create mode 100644 dev-wiki/public/empty-state/search/all-issue-view-dark.webp create mode 100644 dev-wiki/public/empty-state/search/all-issues-view-light.webp create mode 100644 dev-wiki/public/empty-state/search/archive-dark.webp create mode 100644 dev-wiki/public/empty-state/search/archive-light.webp create mode 100644 dev-wiki/public/empty-state/search/comments-dark.webp create mode 100644 dev-wiki/public/empty-state/search/comments-light.webp create mode 100644 dev-wiki/public/empty-state/search/issues-dark.webp create mode 100644 dev-wiki/public/empty-state/search/issues-light.webp create mode 100644 dev-wiki/public/empty-state/search/member-dark.webp create mode 100644 dev-wiki/public/empty-state/search/member-light.webp create mode 100644 dev-wiki/public/empty-state/search/notification-dark.webp create mode 100644 dev-wiki/public/empty-state/search/notification-light.webp create mode 100644 dev-wiki/public/empty-state/search/project-dark.webp create mode 100644 dev-wiki/public/empty-state/search/project-light.webp create mode 100644 dev-wiki/public/empty-state/search/search-dark.webp create mode 100644 dev-wiki/public/empty-state/search/search-light.webp create mode 100644 dev-wiki/public/empty-state/search/snooze-dark.webp create mode 100644 dev-wiki/public/empty-state/search/snooze-light.webp create mode 100644 dev-wiki/public/empty-state/search/views-dark.webp create mode 100644 dev-wiki/public/empty-state/search/views-light.webp create mode 100644 dev-wiki/public/empty-state/state_graph.svg create mode 100644 dev-wiki/public/empty-state/stickies/stickies-dark.webp create mode 100644 dev-wiki/public/empty-state/stickies/stickies-light.webp create mode 100644 dev-wiki/public/empty-state/stickies/stickies-search-dark.webp create mode 100644 dev-wiki/public/empty-state/stickies/stickies-search-light.webp create mode 100644 dev-wiki/public/empty-state/teams/completed-cycles-dark.webp create mode 100644 dev-wiki/public/empty-state/teams/completed-cycles-light.webp create mode 100644 dev-wiki/public/empty-state/teams/current-cycles-dark.webp create mode 100644 dev-wiki/public/empty-state/teams/current-cycles-light.webp create mode 100644 dev-wiki/public/empty-state/teams/dark-upgrade.webp create mode 100644 dev-wiki/public/empty-state/teams/issues-dark.webp create mode 100644 dev-wiki/public/empty-state/teams/issues-light.webp create mode 100644 dev-wiki/public/empty-state/teams/light-upgrade.webp create mode 100644 dev-wiki/public/empty-state/teams/projects-dark.webp create mode 100644 dev-wiki/public/empty-state/teams/projects-light.webp create mode 100644 dev-wiki/public/empty-state/teams/teams-dark.webp create mode 100644 dev-wiki/public/empty-state/teams/teams-light.webp create mode 100644 dev-wiki/public/empty-state/teams/upcoming-cycles-dark.webp create mode 100644 dev-wiki/public/empty-state/teams/upcoming-cycles-light.webp create mode 100644 dev-wiki/public/empty-state/teams/views-dark.webp create mode 100644 dev-wiki/public/empty-state/teams/views-light.webp create mode 100644 dev-wiki/public/empty-state/templates/no-templates-dark.webp create mode 100644 dev-wiki/public/empty-state/templates/no-templates-light.webp create mode 100644 dev-wiki/public/empty-state/templates/upgrade-dark.webp create mode 100644 dev-wiki/public/empty-state/templates/upgrade-light.webp create mode 100644 dev-wiki/public/empty-state/view.svg create mode 100644 dev-wiki/public/empty-state/web-hook.svg create mode 100644 dev-wiki/public/empty-state/workflows/upgrade-dark.webp create mode 100644 dev-wiki/public/empty-state/workflows/upgrade-light.webp create mode 100644 dev-wiki/public/empty-state/worklogs/worklog-dark.png create mode 100644 dev-wiki/public/empty-state/worklogs/worklog-light.png create mode 100644 dev-wiki/public/empty-state/workspace-draft/issue-dark.webp create mode 100644 dev-wiki/public/empty-state/workspace-draft/issue-light.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/api-tokens-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/api-tokens-dark.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/api-tokens-light-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/api-tokens-light.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/exports-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/exports-dark.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/exports-light-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/exports-light.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/imports-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/imports-dark.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/imports-light-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/imports-light.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/integrations-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/integrations-dark.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/integrations-light-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/integrations-light.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/webhooks-dark-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/webhooks-dark.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/webhooks-light-resp.webp create mode 100644 dev-wiki/public/empty-state/workspace-settings/webhooks-light.webp create mode 100644 dev-wiki/public/favicon/android-chrome-192x192.png create mode 100644 dev-wiki/public/favicon/android-chrome-512x512.png create mode 100644 dev-wiki/public/favicon/apple-touch-icon.png create mode 100644 dev-wiki/public/favicon/favicon-16x16.png create mode 100644 dev-wiki/public/favicon/favicon-32x32.png create mode 100644 dev-wiki/public/favicon/favicon.ico create mode 100644 dev-wiki/public/favicon/site.webmanifest create mode 100644 dev-wiki/public/fonts/inter/bold-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/bold.ttf create mode 100644 dev-wiki/public/fonts/inter/heavy-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/heavy.ttf create mode 100644 dev-wiki/public/fonts/inter/light-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/light.ttf create mode 100644 dev-wiki/public/fonts/inter/medium-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/medium.ttf create mode 100644 dev-wiki/public/fonts/inter/regular-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/regular.ttf create mode 100644 dev-wiki/public/fonts/inter/semibold-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/semibold.ttf create mode 100644 dev-wiki/public/fonts/inter/thin-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/thin.ttf create mode 100644 dev-wiki/public/fonts/inter/ultrabold-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/ultrabold.ttf create mode 100644 dev-wiki/public/fonts/inter/ultralight-italic.ttf create mode 100644 dev-wiki/public/fonts/inter/ultralight.ttf create mode 100644 dev-wiki/public/icons/icon-128x128.png create mode 100644 dev-wiki/public/icons/icon-180x180.png create mode 100644 dev-wiki/public/icons/icon-192x192.png create mode 100644 dev-wiki/public/icons/icon-348x348.png create mode 100644 dev-wiki/public/icons/icon-512x512.png create mode 100644 dev-wiki/public/images/logo-spinner-dark.gif create mode 100644 dev-wiki/public/images/logo-spinner-light.gif create mode 100644 dev-wiki/public/instance-not-ready.webp create mode 100644 dev-wiki/public/instance-setup-done.webp create mode 100644 dev-wiki/public/logos/github-black.png create mode 100644 dev-wiki/public/logos/github-dark.svg create mode 100644 dev-wiki/public/logos/github-square.png create mode 100644 dev-wiki/public/logos/github-white.png create mode 100644 dev-wiki/public/logos/gitlab-logo.svg create mode 100644 dev-wiki/public/logos/google-logo.svg create mode 100644 dev-wiki/public/logos/pi.png create mode 100644 dev-wiki/public/mac-command.svg create mode 100644 dev-wiki/public/maintenance-mode.webp create mode 100644 dev-wiki/public/manifest.json create mode 100644 dev-wiki/public/marketplace/connect.svg create mode 100644 dev-wiki/public/og-image.png create mode 100644 dev-wiki/public/onboarding/create-join-workspace-dark.webp create mode 100644 dev-wiki/public/onboarding/create-join-workspace-light.webp create mode 100644 dev-wiki/public/onboarding/cycles.webp create mode 100644 dev-wiki/public/onboarding/invite-members-dark.webp create mode 100644 dev-wiki/public/onboarding/invite-members-light.webp create mode 100644 dev-wiki/public/onboarding/issues.webp create mode 100644 dev-wiki/public/onboarding/modules.webp create mode 100644 dev-wiki/public/onboarding/onboarding-pages.webp create mode 100644 dev-wiki/public/onboarding/pages.webp create mode 100644 dev-wiki/public/onboarding/profile-setup-dark.webp create mode 100644 dev-wiki/public/onboarding/profile-setup-light.webp create mode 100644 dev-wiki/public/onboarding/user-personalization-dark.webp create mode 100644 dev-wiki/public/onboarding/user-personalization-light.webp create mode 100644 dev-wiki/public/onboarding/views.webp create mode 100644 dev-wiki/public/plane-logos/black-horizontal-with-blue-logo.png create mode 100644 dev-wiki/public/plane-logos/blue-without-text.png create mode 100644 dev-wiki/public/plane-logos/plane-business-logo.svg create mode 100644 dev-wiki/public/plane-logos/plane-mobile-pwa.png create mode 100644 dev-wiki/public/plane-logos/plane-one-silver.svg create mode 100644 dev-wiki/public/plane-logos/plane-one.svg create mode 100644 dev-wiki/public/plane-logos/plane-pro-without-text.svg create mode 100644 dev-wiki/public/plane-logos/plane-pro.svg create mode 100644 dev-wiki/public/plane-logos/white-horizontal-with-blue-logo.png create mode 100644 dev-wiki/public/plane-logos/white-horizontal.svg create mode 100644 dev-wiki/public/plane-takeoff.png create mode 100644 dev-wiki/public/projects/dark-upgrade.svg create mode 100644 dev-wiki/public/projects/light-upgrade.svg create mode 100644 dev-wiki/public/projects/project-states-dark.webp create mode 100644 dev-wiki/public/projects/project-states-light.webp create mode 100644 dev-wiki/public/projects/states-dark.svg create mode 100644 dev-wiki/public/projects/states-light.svg create mode 100644 dev-wiki/public/services/asana.svg create mode 100644 dev-wiki/public/services/csv.svg create mode 100644 dev-wiki/public/services/excel.svg create mode 100644 dev-wiki/public/services/flatfile.svg create mode 100644 dev-wiki/public/services/github-dark.svg create mode 100644 dev-wiki/public/services/github-light.svg create mode 100644 dev-wiki/public/services/github.png create mode 100644 dev-wiki/public/services/github.svg create mode 100644 dev-wiki/public/services/gitlab.svg create mode 100644 dev-wiki/public/services/jira.svg create mode 100644 dev-wiki/public/services/json.svg create mode 100644 dev-wiki/public/services/linear.svg create mode 100644 dev-wiki/public/services/slack.png create mode 100644 dev-wiki/public/site.webmanifest.json create mode 100644 dev-wiki/public/sw.js create mode 100644 dev-wiki/public/upcoming-features/imports-cta-1-dark.png create mode 100644 dev-wiki/public/upcoming-features/imports-cta-1-light.png create mode 100644 dev-wiki/public/upcoming-features/imports-cta-2-dark.png create mode 100644 dev-wiki/public/upcoming-features/imports-cta-2-light.png create mode 100644 dev-wiki/public/upcoming-features/integrations-cta-1-dark.png create mode 100644 dev-wiki/public/upcoming-features/integrations-cta-1-light.png create mode 100644 dev-wiki/public/upcoming-features/integrations-cta-2-dark.png create mode 100644 dev-wiki/public/upcoming-features/integrations-cta-2-light.png create mode 100644 dev-wiki/public/user.png create mode 100644 dev-wiki/public/users/user-1.png create mode 100644 dev-wiki/public/users/user-2.png create mode 100644 dev-wiki/public/users/user-profile-cover-default-img.png create mode 100644 dev-wiki/public/users/user.png create mode 100644 dev-wiki/public/workbox-91d218dd.js create mode 100644 dev-wiki/public/workspace-active-cycles/cta-l-1-dark.webp create mode 100644 dev-wiki/public/workspace-active-cycles/cta-l-1-light.webp create mode 100644 dev-wiki/public/workspace-active-cycles/cta-r-1-dark.webp create mode 100644 dev-wiki/public/workspace-active-cycles/cta-r-1-light.webp create mode 100644 dev-wiki/public/workspace-active-cycles/cta-r-2-dark.webp create mode 100644 dev-wiki/public/workspace-active-cycles/cta-r-2-light.webp create mode 100644 dev-wiki/public/workspace/workspace-creation-disabled.png create mode 100644 dev-wiki/public/workspace/workspace-not-available.png create mode 100644 dev-wiki/styles/command-pallette.css create mode 100644 dev-wiki/styles/emoji.css create mode 100644 dev-wiki/styles/globals.css create mode 100644 dev-wiki/styles/nprogress.css create mode 100644 dev-wiki/styles/react-day-picker.css create mode 100644 dev-wiki/tailwind.config.js create mode 100644 dev-wiki/tsconfig.json create mode 100644 dev-wiki/use-font-face-observer.d.ts diff --git a/dev-wiki/.env.example b/dev-wiki/.env.example new file mode 100644 index 0000000000..15d7a36a9d --- /dev/null +++ b/dev-wiki/.env.example @@ -0,0 +1,12 @@ +NEXT_PUBLIC_API_BASE_URL="http://localhost:8000" + +NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000" + +NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001" +NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" + +NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002" +NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" + +NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100" +NEXT_PUBLIC_LIVE_BASE_PATH="/live" diff --git a/dev-wiki/.eslintignore b/dev-wiki/.eslintignore new file mode 100644 index 0000000000..84f01402dd --- /dev/null +++ b/dev-wiki/.eslintignore @@ -0,0 +1,3 @@ +.next/* +out/* +public/* \ No newline at end of file diff --git a/dev-wiki/.eslintrc.js b/dev-wiki/.eslintrc.js new file mode 100644 index 0000000000..5a6f060678 --- /dev/null +++ b/dev-wiki/.eslintrc.js @@ -0,0 +1,6 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["@plane/eslint-config/next.js"], + parser: "@typescript-eslint/parser", +}; diff --git a/dev-wiki/.prettierignore b/dev-wiki/.prettierignore new file mode 100644 index 0000000000..e841c6b328 --- /dev/null +++ b/dev-wiki/.prettierignore @@ -0,0 +1,5 @@ +.next +.turbo +out/ +dist/ +build/ \ No newline at end of file diff --git a/dev-wiki/.prettierrc b/dev-wiki/.prettierrc new file mode 100644 index 0000000000..87d988f1b2 --- /dev/null +++ b/dev-wiki/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/dev-wiki/.yarnrc.yml b/dev-wiki/.yarnrc.yml new file mode 100644 index 0000000000..f53151fc10 --- /dev/null +++ b/dev-wiki/.yarnrc.yml @@ -0,0 +1,4 @@ +packageExtensions: + "@react-pdf/pdfkit@*": + dependencies: + "pako": "*" diff --git a/dev-wiki/Dockerfile.dev b/dev-wiki/Dockerfile.dev new file mode 100644 index 0000000000..64465755ee --- /dev/null +++ b/dev-wiki/Dockerfile.dev @@ -0,0 +1,12 @@ +FROM node:20-alpine +RUN apk add --no-cache libc6-compat +# Set working directory +WORKDIR /app + + +COPY . . +RUN yarn global add turbo +RUN yarn install +EXPOSE 3000 +VOLUME [ "/app/node_modules", "/app/web/node_modules" ] +CMD ["yarn", "dev", "--filter=web"] diff --git a/dev-wiki/Dockerfile.web b/dev-wiki/Dockerfile.web new file mode 100644 index 0000000000..ed206c2989 --- /dev/null +++ b/dev-wiki/Dockerfile.web @@ -0,0 +1,112 @@ +FROM node:20-alpine as base + +# ***************************************************************************** +# STAGE 1: Build the project +# ***************************************************************************** +FROM base AS builder +RUN apk add --no-cache libc6-compat +# Set working directory +WORKDIR /app + +RUN yarn global add turbo +COPY . . + +RUN turbo prune --scope=web --docker + +# ***************************************************************************** +# STAGE 2: Install dependencies & build the project +# ***************************************************************************** +# Add lockfile and package.json's of isolated subworkspace +FROM base AS installer + +RUN apk add --no-cache libc6-compat +WORKDIR /app + +# First install the dependencies (as they change less often) +COPY .gitignore .gitignore +COPY --from=builder /app/out/json/ . +COPY --from=builder /app/out/yarn.lock ./yarn.lock +RUN yarn install --network-timeout 500000 + +# Build the project +COPY --from=builder /app/out/full/ . +COPY turbo.json turbo.json + +ARG NEXT_PUBLIC_API_BASE_URL="" +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + +ARG NEXT_PUBLIC_ADMIN_BASE_URL="" +ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL + +ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" +ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH + +ARG NEXT_PUBLIC_LIVE_BASE_URL="" +ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL + +ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live" +ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH + +ARG NEXT_PUBLIC_SPACE_BASE_URL="" +ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL + +ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" +ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH + +ARG NEXT_PUBLIC_WEB_BASE_URL="" +ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL + +ARG NEXT_PUBLIC_SILO_BASE_URL="" +ENV NEXT_PUBLIC_SILO_BASE_URL=$NEXT_PUBLIC_SILO_BASE_URL + +ARG NEXT_PUBLIC_SILO_BASE_PATH="/silo" +ENV NEXT_PUBLIC_SILO_BASE_PATH=$NEXT_PUBLIC_SILO_BASE_PATH + +ENV NEXT_TELEMETRY_DISABLED 1 +ENV TURBO_TELEMETRY_DISABLED 1 + +RUN yarn turbo run build --filter=web + +# ***************************************************************************** +# STAGE 3: Copy the project and start it +# ***************************************************************************** +FROM base AS runner +WORKDIR /app + +COPY --from=installer /app/web/next.config.js . +COPY --from=installer /app/web/package.json . + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --from=installer /app/web/.next/standalone ./ +COPY --from=installer /app/web/.next ./web/.next +COPY --from=installer /app/web/public ./web/public + +ARG NEXT_PUBLIC_API_BASE_URL="" +ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL + +ARG NEXT_PUBLIC_ADMIN_BASE_URL="" +ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL + +ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode" +ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH + +ARG NEXT_PUBLIC_LIVE_BASE_URL="" +ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL + +ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live" +ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_BASE_PATH + +ARG NEXT_PUBLIC_SPACE_BASE_URL="" +ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL + +ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces" +ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH + +ARG NEXT_PUBLIC_WEB_BASE_URL="" +ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL + +ENV NEXT_TELEMETRY_DISABLED 1 +ENV TURBO_TELEMETRY_DISABLED 1 + +EXPOSE 3000 diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/archived/layout.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/archived/layout.tsx new file mode 100644 index 0000000000..ce3c314c47 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/archived/layout.tsx @@ -0,0 +1,17 @@ +"use client"; + +// components +import { AppHeader } from "@/components/core"; +// local components +import { PageTypeFiltersHeader } from "../filters-header"; +import { PageTypeHeader } from "../header"; + +export default function ArchivedPagesListLayout({ children }: { children: React.ReactNode }) { + return ( + <> + } /> + + {children} + + ); +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/archived/page.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/archived/page.tsx new file mode 100644 index 0000000000..f48d31a734 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/archived/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +// plane web components +import { WikiPagesListLayoutRoot } from "@/plane-web/components/pages"; + +export default function ArchivedPagesList() { + return ; +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/filters-header.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/filters-header.tsx new file mode 100644 index 0000000000..8d0ca39ab9 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/filters-header.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { useCallback, useEffect } from "react"; +import { observer } from "mobx-react"; +import { usePathname } from "next/navigation"; +// types +import { TPageFilterProps } from "@plane/types"; +// components +import { PageAppliedFiltersList } from "@/components/pages"; +// helpers +import { calculateTotalFilters } from "@/helpers/filter.helper"; +import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store/use-page-store"; +// plane web hooks + +export const PageTypeFiltersHeader = observer(() => { + // params + const pathname = usePathname(); + // store hooks + const { filters, updateFilters, clearAllFilters } = usePageStore(EPageStoreType.WORKSPACE); + // derived values + const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0; + // handle remove filter + const handleRemoveFilter = useCallback( + (key: keyof TPageFilterProps, value: string | null) => { + let newValues = filters.filters?.[key]; + + if (key === "favorites") newValues = !!value; + if (Array.isArray(newValues)) { + if (!value) newValues = []; + else newValues = newValues.filter((val) => val !== value); + } + + updateFilters("filters", { [key]: newValues }); + }, + [filters.filters, updateFilters] + ); + + useEffect(() => { + clearAllFilters(); + updateFilters("searchQuery", ""); + }, [clearAllFilters, pathname, updateFilters]); + + if (!isFiltersApplied) return null; + + return ( +
+ +
+ ); +}); diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/header.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/header.tsx new file mode 100644 index 0000000000..fc905ff4f5 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/header.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { observer } from "mobx-react"; +import { useParams, usePathname, useRouter } from "next/navigation"; +import { FileText } from "lucide-react"; +// plane imports +import { EPageAccess } from "@plane/constants"; +import { TPage, TPageNavigationTabs } from "@plane/types"; +// ui +import { Breadcrumbs, Button, Header, setToast, TOAST_TYPE } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common"; +// import { FiltersDropdown } from "@/components/issues"; +import { PageOrderByDropdown, PageSearchInput } from "@/components/pages"; +// helpers +import { calculateTotalFilters } from "@/helpers/filter.helper"; +import { capitalizeFirstLetter } from "@/helpers/string.helper"; +// hooks +// plane web hooks +import { useMember } from "@/hooks/store/use-member"; +import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store/use-page-store"; + +type Props = { + pageType: TPageNavigationTabs; +}; + +export const PageTypeHeader: React.FC = observer((props) => { + const { pageType } = props; + // states + const [isCreatingPage, setIsCreatingPage] = useState(false); + // params + const router = useRouter(); + const { workspaceSlug } = useParams(); + const pathname = usePathname(); + // store hooks + const { + workspace: { workspaceMemberIds }, + } = useMember(); + const { createPage, filters, updateFilters, clearAllFilters } = usePageStore(EPageStoreType.WORKSPACE); + // derived values + const isFiltersApplied = calculateTotalFilters(filters?.filters ?? {}) !== 0; + // handle page create + const handleCreatePage = async () => { + setIsCreatingPage(true); + + const payload: Partial = { + access: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC, + }; + + await createPage(payload) + .then((res) => { + const pageId = `/${workspaceSlug}/pages/${res?.id}`; + router.push(pageId); + }) + .catch((err: { data: { error: any } }) => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: err?.data?.error || "Page could not be created. Please try again.", + }) + ) + .finally(() => setIsCreatingPage(false)); + }; + + useEffect(() => { + clearAllFilters(); + updateFilters("searchQuery", ""); + }, [clearAllFilters, pathname, updateFilters]); + + return ( +
+ +
+ + } + /> + } + /> + } + /> + +
+
+ + updateFilters("searchQuery", val)} + /> + { + if (val.key) updateFilters("sortKey", val.key); + if (val.order) updateFilters("sortBy", val.order); + }} + /> + {/* } */} + {/* title="Filters" */} + {/* placement="bottom-end" */} + {/* isFiltersApplied={isFiltersApplied} */} + {/* > */} + {/* */} + {/* */} + + +
+ ); +}); diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/private/layout.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/private/layout.tsx new file mode 100644 index 0000000000..d2615fb77b --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/private/layout.tsx @@ -0,0 +1,17 @@ +"use client"; + +// components +import { AppHeader } from "@/components/core"; +// local components +import { PageTypeFiltersHeader } from "../filters-header"; +import { PageTypeHeader } from "../header"; + +export default function PrivatePagesListLayout({ children }: { children: React.ReactNode }) { + return ( + <> + } /> + + {children} + + ); +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/private/page.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/private/page.tsx new file mode 100644 index 0000000000..0fc8b39614 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/private/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +// plane web components +import { WikiPagesListLayoutRoot } from "@/plane-web/components/pages"; + +export default function PrivatePagesList() { + return ; +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/public/layout.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/public/layout.tsx new file mode 100644 index 0000000000..69d9433562 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/public/layout.tsx @@ -0,0 +1,17 @@ +"use client"; + +// components +import { AppHeader } from "@/components/core"; +// local components +import { PageTypeFiltersHeader } from "../filters-header"; +import { PageTypeHeader } from "../header"; + +export default function PublicPagesListLayout({ children }: { children: React.ReactNode }) { + return ( + <> + } /> + + {children} + + ); +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/public/page.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/public/page.tsx new file mode 100644 index 0000000000..96a4f722e2 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/(pageType)/public/page.tsx @@ -0,0 +1,8 @@ +"use client"; + +// plane web components +import { WikiPagesListLayoutRoot } from "@/plane-web/components/pages"; + +export default function PublicPagesList() { + return ; +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/header.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/header.tsx new file mode 100644 index 0000000000..a57d8e3629 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/header.tsx @@ -0,0 +1,231 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { observer } from "mobx-react"; +import { useParams, useRouter } from "next/navigation"; +import useSWR from "swr"; +import { FileText, ChevronRight } from "lucide-react"; +// constants +import { EUserWorkspaceRoles, EUserPermissionsLevel } from "@plane/constants"; +// types +import { TPage } from "@plane/types"; +// ui +import { Button, CustomMenu, Header } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common"; +import { PageBreadcrumbItem } from "@/components/pages"; +import { PageHeaderActions } from "@/components/pages/header/actions"; +import { PageSyncingBadge } from "@/components/pages/header/syncing-badge"; +// helpers +import { SPACE_BASE_PATH, SPACE_BASE_URL } from "@/helpers/common.helper"; +// hooks +import { useUserPermissions } from "@/hooks/store"; +// plane web components +import { PublishPageModal, CollaboratorsList } from "@/plane-web/components/pages"; +// plane web hooks +import { EPageStoreType, usePage, usePageStore, usePublishPage } from "@/plane-web/hooks/store"; + +const storeType = EPageStoreType.WORKSPACE; + +export interface IPagesHeaderProps { + showButton?: boolean; +} + +export const PageDetailsHeader = observer(() => { + // states + const [isPublishModalOpen, setIsPublishModalOpen] = useState(false); + // params + const { workspaceSlug, pageId } = useParams(); + const router = useRouter(); + // store hooks + const { allowPermissions } = useUserPermissions(); + const { fetchParentPages } = usePageStore(storeType); + const page = usePage({ + pageId: pageId?.toString() ?? "", + storeType, + }); + const { fetchWorkspacePagePublishSettings, getPagePublishSettings, publishWorkspacePage, unpublishWorkspacePage } = + usePublishPage(); + const { anchor, isCurrentUserOwner } = page ?? {}; + // derived values + const isDeployed = !!anchor; + const pagePublishSettings = getPagePublishSettings(pageId.toString()); + const isPublishAllowed = + isCurrentUserOwner || allowPermissions([EUserWorkspaceRoles.ADMIN], EUserPermissionsLevel.WORKSPACE); + + // Always fetch parent pages to keep the cache up-to-date + const { data: parentPagesList, isLoading: isParentPagesLoading } = useSWR( + workspaceSlug && pageId ? `WORKSPACE_PARENT_PAGES_LIST_${pageId.toString()}` : null, + workspaceSlug && pageId ? () => fetchParentPages(pageId.toString()) : null + ); + + // Transform the data once it's available using useMemo + const orderedParentPages = useMemo(() => { + if (!parentPagesList) return undefined; + return createOrderedParentChildArray(parentPagesList); + }, [parentPagesList]); + + // Now use orderedParentPages instead of parentPagesList for your UI logic + const isRootPage = orderedParentPages?.length === 1; + const rootParentDetails = orderedParentPages?.[0]; // First item is the root + const middleParents = orderedParentPages?.slice(1, -1) ?? []; // Middle items (excluding root and current) + + const SPACE_APP_URL = SPACE_BASE_URL.trim() === "" ? window.location.origin : SPACE_BASE_URL; + const publishLink = `${SPACE_APP_URL}${SPACE_BASE_PATH}/pages/${anchor}`; + + const BreadcrumbSeparator = () => ( +
+ +
+ ); + + if (!page) return null; + + return ( + <> + await fetchWorkspacePagePublishSettings(pageId.toString())} + isOpen={isPublishModalOpen} + onClose={() => setIsPublishModalOpen(false)} + pagePublishSettings={pagePublishSettings} + publishPage={(data) => publishWorkspacePage(pageId.toString(), data)} + unpublishPage={() => unpublishWorkspacePage(pageId.toString())} + /> +
+ +
+
+
+ } + /> +
+ + {isParentPagesLoading ? ( +
+ +
+
+
+
+ ) : ( + <> + {!isRootPage && rootParentDetails?.id && ( +
+ + +
+ )} + + {middleParents.length > 0 && ( +
+ + + {middleParents.map( + (parent, index) => + parent.id && ( +
+ router.push(`/${workspaceSlug}/pages/${parent.id}`)} + > + + +
+ ) + )} +
+
+ )} + + )} + +
+ + +
+
+
+ + +
+ + + {isDeployed && ( + + + Live + + )} + {isPublishAllowed && ( + + )} + +
+
+
+ + ); +}); + +function createOrderedParentChildArray(parentPagesList: TPage[]) { + // If the list is empty or has only one item, return it as is + if (!parentPagesList || parentPagesList.length <= 1) { + return parentPagesList; + } + + // Create a map for quick lookups by ID + const pagesMap = new Map(); + parentPagesList.forEach((page) => { + pagesMap.set(page.id, page); + }); + + const rootPage = parentPagesList.find((page) => page.parent_id === null); + + if (!rootPage) { + console.error("No root page found in the list"); + return parentPagesList; + } + + const result: TPage[] = []; + + function buildHierarchy(currentPage: TPage) { + result.push(currentPage); + + // Find all direct children of the current page + const children = parentPagesList.filter((page) => page.parent_id === currentPage.id); + + // Process each child + children.forEach((child) => { + buildHierarchy(child); + }); + } + + // Start building from the root + buildHierarchy(rootPage); + + return result; +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/layout.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/layout.tsx new file mode 100644 index 0000000000..4d71b7ef4a --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/layout.tsx @@ -0,0 +1,15 @@ +"use client"; + +// components +import { AppHeader, ContentWrapper } from "@/components/core"; +// layout +import { PageDetailsHeader } from "./header"; + +export default function PageDetailsLayout({ children }: { children: React.ReactNode }) { + return ( + <> + } /> + {children} + + ); +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/page.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/page.tsx new file mode 100644 index 0000000000..c4449ed880 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/[pageId]/page.tsx @@ -0,0 +1,193 @@ +"use client"; + +import { useCallback, useEffect, useMemo } from "react"; +import { observer } from "mobx-react-lite"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import useSWR from "swr"; +// plane types +import { TSearchEntityRequestPayload, TWebhookConnectionQueryParams } from "@plane/types"; +import { EFileAssetType } from "@plane/types/src/enums"; +// ui +import { getButtonStyling } from "@plane/ui"; +// components +import { LogoSpinner } from "@/components/common"; +import { PageHead } from "@/components/core"; +// import { IssuePeekOverview } from "@/components/issues"; // Removed for dev-wiki - not needed for pages +import { PageRoot, TPageRootConfig, TPageRootHandlers } from "@/components/pages"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useEditorConfig } from "@/hooks/editor"; +import { useEditorAsset, useWorkspace } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +// plane web hooks +import { EPageStoreType, usePage, usePageStore } from "@/plane-web/hooks/store"; +// plane web services +import { WorkspaceService } from "@/plane-web/services"; +import { WorkspacePageService, WorkspacePageVersionService } from "@/plane-web/services/page"; +// services +const workspaceService = new WorkspaceService(); +const workspacePageService = new WorkspacePageService(); +const workspacePageVersionService = new WorkspacePageVersionService(); + +const storeType = EPageStoreType.WORKSPACE; + +const PageDetailsPage = observer(() => { + // router + const router = useAppRouter(); + const { workspaceSlug, pageId } = useParams(); + // flag + // store hooks + const { getWorkspaceBySlug } = useWorkspace(); + const { createPage, fetchPageDetails, isNestedPagesEnabled } = usePageStore(storeType); + const page = usePage({ + pageId: pageId?.toString() ?? "", + storeType, + }); + const { uploadEditorAsset } = useEditorAsset(); + // derived values + const workspaceId = useMemo( + () => (workspaceSlug ? (getWorkspaceBySlug(workspaceSlug?.toString())?.id ?? "") : ""), + [getWorkspaceBySlug, workspaceSlug] + ); + const { canCurrentUserAccessPage, id, name, updateDescription } = page ?? {}; + // entity search handler + const fetchEntityCallback = useCallback( + async (payload: TSearchEntityRequestPayload) => + await workspaceService.searchEntity(workspaceSlug?.toString() ?? "", payload), + [workspaceSlug] + ); + // editor config + const { getEditorFileHandlers } = useEditorConfig(); + // fetch page details + const { error: pageDetailsError } = useSWR( + pageId ? `PAGE_DETAILS_${pageId}` : null, + pageId ? () => fetchPageDetails(pageId.toString()) : null, + { + revalidateIfStale: true, + revalidateOnFocus: true, + revalidateOnReconnect: true, + } + ); + // page root handlers + const pageRootHandlers: TPageRootHandlers = useMemo( + () => ({ + create: createPage, + fetchAllVersions: async (pageId) => { + if (!workspaceSlug) return; + return await workspacePageVersionService.fetchAllVersions(workspaceSlug.toString(), pageId); + }, + fetchDescriptionBinary: async () => { + if (!workspaceSlug || !id) return; + return await workspacePageService.fetchDescriptionBinary(workspaceSlug.toString(), id); + }, + fetchEntity: fetchEntityCallback, + fetchVersionDetails: async (pageId, versionId) => { + if (!workspaceSlug) return; + return await workspacePageVersionService.fetchVersionById(workspaceSlug.toString(), pageId, versionId); + }, + getRedirectionLink: (pageId) => { + if (pageId) { + return `/${workspaceSlug}/pages/${pageId}`; + } else { + return `/${workspaceSlug}/pages`; + } + }, + updateDescription: updateDescription ?? (async () => {}), + }), + [createPage, fetchEntityCallback, id, updateDescription, workspaceSlug] + ); + // page root config + const pageRootConfig: TPageRootConfig = useMemo( + () => ({ + fileHandler: getEditorFileHandlers({ + uploadFile: async (blockId, file) => { + const { asset_id } = await uploadEditorAsset({ + blockId, + file, + data: { + entity_identifier: id ?? "", + entity_type: EFileAssetType.PAGE_DESCRIPTION, + }, + workspaceSlug: workspaceSlug?.toString() ?? "", + }); + return asset_id; + }, + workspaceId, + workspaceSlug: workspaceSlug?.toString() ?? "", + }), + }), + [getEditorFileHandlers, id, uploadEditorAsset, workspaceId, workspaceSlug] + ); + + const webhookConnectionParams: TWebhookConnectionQueryParams = useMemo( + () => ({ + documentType: "workspace_page", + workspaceSlug: workspaceSlug?.toString() ?? "", + }), + [workspaceSlug] + ); + + useEffect(() => { + if (page?.deleted_at && page?.id) { + router.push(pageRootHandlers.getRedirectionLink()); + } + }, [page?.deleted_at, page?.id, router, pageRootHandlers]); + + if ((!page || !id) && !pageDetailsError) + return ( +
+ +
+ ); + + if (!isNestedPagesEnabled(workspaceSlug?.toString()) && page?.parent_id) + return ( +
+

Please upgrade your plan to view this nested page

+

+ Please upgrade your plan to view this nested page +

+ + View other Pages + +
+ ); + + if (pageDetailsError || !canCurrentUserAccessPage) + return ( +
+

Page not found

+

+ The page you are trying to access doesn{"'"}t exist or you don{"'"}t have permission to view it. +

+ + View other Pages + +
+ ); + + if (!page) return null; + + return ( + <> + +
+
+ + {/* */} +
+
+ + ); +}); + +export default PageDetailsPage; diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/header.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/header.tsx new file mode 100644 index 0000000000..63873a7c1d --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/header.tsx @@ -0,0 +1,48 @@ +"use client"; + +import Image from "next/image"; +import { useTheme } from "next-themes"; +import { Home } from "lucide-react"; +// images +import githubBlackImage from "/public/logos/github-black.png"; +import githubWhiteImage from "/public/logos/github-white.png"; +// plane imports +import { Breadcrumbs, Header } from "@plane/ui"; +// components +import { BreadcrumbLink } from "@/components/common"; + +export const PagesAppDashboardHeader = () => { + // hooks + const { resolvedTheme } = useTheme(); + + return ( +
+ +
+ + } />} + /> + +
+
+ + + GitHub Logo + Star us on GitHub + + +
+ ); +}; diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/layout.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/layout.tsx new file mode 100644 index 0000000000..81bbd524e5 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/layout.tsx @@ -0,0 +1,52 @@ +"use client"; + +// layouts +import { useParams } from "next/navigation"; +// components +import { EUserPermissions } from "@plane/constants"; +// wrappers +import WorkspaceAccessWrapper from "@/layouts/access/workspace-wrapper"; +import { AuthenticationWrapper } from "@/lib/wrappers"; +// plane web components +// import { PagesAppCommandPalette } from "@/plane-web/components/command-palette"; +import { WithFeatureFlagHOC } from "@/plane-web/components/feature-flags"; +import { WorkspacePagesUpgrade } from "@/plane-web/components/pages"; +// plane web layouts +import { WorkspaceAuthWrapper } from "@/plane-web/layouts/workspace-wrapper"; +// local components +// import { FloatingActionsRoot } from "../../(projects)/floating-action-bar"; +import { PagesAppSidebar } from "./sidebar"; + +export default function WorkspacePagesLayout({ children }: { children: React.ReactNode }) { + // router + const { workspaceSlug } = useParams(); + + return ( + + + } + > + + {/* */} +
+ +
+ {children} +
+ {/* */} + {/* */} + {/* */} +
+
+
+
+
+ ); +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/page.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/page.tsx new file mode 100644 index 0000000000..d395b0984b --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/page.tsx @@ -0,0 +1,41 @@ +"use client"; + +import { useParams } from "next/navigation"; +// components +import { AppHeader } from "@/components/core"; +// import { RecentActivityWidget } from "@/components/home"; +import { UserGreetingsView } from "@/components/user"; +// hooks +import { useUser } from "@/hooks/store"; +// plane web components +import { PagesAppDashboardHeader } from "./header"; + +export default function WorkspacePagesPage() { + // navigation + const { workspaceSlug } = useParams(); + // store hooks + const { data: currentUser } = useUser(); + + return ( + <> + } /> +
+ {currentUser && } + {workspaceSlug && ( +
+
+ {/* */} +
+ {/*
*/} + {/* */} + {/*
*/} +
+ )} +
+ + ); +} diff --git a/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/sidebar.tsx b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/sidebar.tsx new file mode 100644 index 0000000000..5331da283b --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/(pages)/pages/sidebar.tsx @@ -0,0 +1,112 @@ +import { useRef, useState, useEffect, useCallback } from "react"; +import { observer } from "mobx-react"; +import { useParams, usePathname } from "next/navigation"; +import useSWR from "swr"; +// plane helpers +import { useOutsideClickDetector } from "@plane/hooks"; +// components +import { SidebarDropdown } from "@/components/workspace"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useAppTheme } from "@/hooks/store"; +// plane web components +import { PagesAppSidebarList, PagesAppSidebarMenu, PagesAppSidebarQuickActions } from "@/plane-web/components/pages"; +// import { SidebarAppSwitcher } from "@/plane-web/components/sidebar"; +// plane web hooks +import { usePageStore, EPageStoreType } from "@/plane-web/hooks/store/use-page-store"; + +export const PagesAppSidebar = observer(() => { + // params + const { workspaceSlug, pageId } = useParams(); + const pathname = usePathname(); + // state + const [expandedPageIds, setExpandedPageIds] = useState([]); + // store hooks + const { toggleSidebar, sidebarCollapsed } = useAppTheme(); + const { fetchParentPages } = usePageStore(EPageStoreType.WORKSPACE); + // refs + const ref = useRef(null); + + // Fetch parent pages if we're on a page detail view + const { data: parentPagesList } = useSWR( + workspaceSlug && pageId && pathname?.includes("/pages/") ? `PARENT_PAGES_LIST_${pageId.toString()}` : null, + workspaceSlug && pageId && pathname?.includes("/pages/") ? () => fetchParentPages(pageId.toString()) : null + ); + + // Optimized parent pages expansion + const handleParentPagesExpansion = useCallback((parentPages: any[]) => { + if (!parentPages || parentPages.length === 0) return; + + // Extract all valid page IDs from the parent chain + const parentIds = parentPages.map((page) => page.id).filter((id) => id !== undefined) as string[]; + + // Only add IDs that aren't already in the expanded list + setExpandedPageIds((prev) => { + // Create a Set for efficient lookup + const existingIds = new Set(prev); + let hasChanges = false; + + // Check each parent ID + parentIds.forEach((id) => { + if (!existingIds.has(id)) { + existingIds.add(id); + hasChanges = true; + } + }); + + // Only create a new array if changes were made + return hasChanges ? Array.from(existingIds) : prev; + }); + }, []); + + // Only expand parent pages when the page changes + useEffect(() => { + if (parentPagesList && parentPagesList.length > 0) { + handleParentPagesExpansion(parentPagesList); + } + }, [parentPagesList, handleParentPagesExpansion]); + + useOutsideClickDetector(ref, () => { + if (sidebarCollapsed === false) { + if (window.innerWidth < 768) { + toggleSidebar(); + } + } + }); + + return ( +
+
+ +
+ {/* */} + +
+ +
+ +
+
+ ); +}); diff --git a/dev-wiki/app/(all)/[workspaceSlug]/layout.tsx b/dev-wiki/app/(all)/[workspaceSlug]/layout.tsx new file mode 100644 index 0000000000..5640b62134 --- /dev/null +++ b/dev-wiki/app/(all)/[workspaceSlug]/layout.tsx @@ -0,0 +1,3 @@ +export default function WorkspaceLayout({ children }: { children: React.ReactNode }) { + return <>{children}; +} diff --git a/dev-wiki/app/(all)/layout.preload.tsx b/dev-wiki/app/(all)/layout.preload.tsx new file mode 100644 index 0000000000..18ca3b4b38 --- /dev/null +++ b/dev-wiki/app/(all)/layout.preload.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { useEffect } from "react"; +import ReactDOM from "react-dom"; + +// https://nextjs.org/docs/app/api-reference/functions/generate-metadata#link-relpreload +export const usePreloadResources = () => { + useEffect(() => { + const preloadItem = (url: string) => { + ReactDOM.preload(url, { as: "fetch", crossOrigin: "use-credentials" }); + }; + + const urls = [ + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/instances/`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/profile/`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/settings/`, + `${process.env.NEXT_PUBLIC_API_BASE_URL}/api/users/me/workspaces/?v=${Date.now()}`, + ]; + + urls.forEach(url => preloadItem(url)); + }, []); +}; + +export const PreloadResources = () => { + usePreloadResources(); + return null; +}; diff --git a/dev-wiki/app/(all)/layout.tsx b/dev-wiki/app/(all)/layout.tsx new file mode 100644 index 0000000000..32589c4bf0 --- /dev/null +++ b/dev-wiki/app/(all)/layout.tsx @@ -0,0 +1,31 @@ +import { Metadata, Viewport } from "next"; + +import { PreloadResources } from "./layout.preload"; + +// styles +import "@/styles/command-pallette.css"; +import "@/styles/emoji.css"; +import "@/styles/react-day-picker.css"; + +export const metadata: Metadata = { + robots: { + index: false, + follow: false, + }, +}; + +export const viewport: Viewport = { + minimumScale: 1, + initialScale: 1, + width: "device-width", + viewportFit: "cover", +}; + +export default function AppLayout({ children }: { children: React.ReactNode }) { + return ( + <> + + {children} + + ); +} diff --git a/dev-wiki/app/error.tsx b/dev-wiki/app/error.tsx new file mode 100644 index 0000000000..348bc6b64a --- /dev/null +++ b/dev-wiki/app/error.tsx @@ -0,0 +1,77 @@ +"use client"; + +// ui +import Link from "next/link"; +import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui"; +// helpers +import { cn } from "@plane/utils"; +import { API_BASE_URL } from "@/helpers/common.helper"; +// hooks +import { useAppRouter } from "@/hooks/use-app-router"; +// layouts +import DefaultLayout from "@/layouts/default-layout"; +// services +import { AuthService } from "@/services/auth.service"; + +// services +const authService = new AuthService(); + +export default function CustomErrorComponent() { + const router = useAppRouter(); + + const handleRefresh = () => { + window.location.reload(); + }; + + const handleSignOut = async () => { + await authService + .signOut(API_BASE_URL) + .catch(() => + setToast({ + type: TOAST_TYPE.ERROR, + title: "Error!", + message: "Failed to sign out. Please try again.", + }) + ) + .finally(() => router.push("/")); + }; + + return ( + +
+
+
+
+

Yikes! That doesn{"'"}t look good.

+

+ That crashed Plane, pun intended. No worries, though. Our engineers have been notified. If you have more + details, please write to{" "} + + support@plane.so + {" "} + or on our{" "} + + Discord + + . +

+
+
+ + Go to home + + +
+
+
+
+
+ ); +} diff --git a/dev-wiki/app/global-error.tsx b/dev-wiki/app/global-error.tsx new file mode 100644 index 0000000000..b0b191e411 --- /dev/null +++ b/dev-wiki/app/global-error.tsx @@ -0,0 +1,17 @@ +"use client"; + +import NextError from "next/error"; + +export default function GlobalError() { + return ( + + + {/* `NextError` is the default Next.js error page component. Its type + definition requires a `statusCode` prop. However, since the App Router + does not expose status codes for errors, we simply pass 0 to render a + generic error message. */} + + + + ); +} diff --git a/dev-wiki/app/layout.tsx b/dev-wiki/app/layout.tsx new file mode 100644 index 0000000000..db702d5ca0 --- /dev/null +++ b/dev-wiki/app/layout.tsx @@ -0,0 +1,105 @@ +import { Metadata, Viewport } from "next"; +import Script from "next/script"; +// styles +import "@/styles/globals.css"; +import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; +// helpers +import { cn } from "@/helpers/common.helper"; +// local +import { AppProvider } from "./provider"; + +export const metadata: Metadata = { + title: "Plane | Simple, extensible, open-source project management tool.", + description: SITE_DESCRIPTION, + metadataBase: new URL("https://app.plane.so"), + openGraph: { + title: "Plane | Simple, extensible, open-source project management tool.", + description: "Open-source project management tool to manage work items, cycles, and product roadmaps easily", + url: "https://app.plane.so/", + images: [ + { + url: "/og-image.png", + width: 1200, + height: 630, + alt: "Plane - Modern project management", + }, + ], + }, + keywords: + "software development, plan, ship, software, accelerate, code management, release management, project management, work item tracking, agile, scrum, kanban, collaboration", + twitter: { + site: "@planepowers", + card: "summary_large_image", + images: [ + { + url: "/og-image.png", + width: 1200, + height: 630, + alt: "Plane - Modern project management", + }, + ], + }, +}; + +export const viewport: Viewport = { + minimumScale: 1, + initialScale: 1, + maximumScale: 1, + userScalable: false, + width: "device-width", + viewportFit: "cover", +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + const isSessionRecorderEnabled = parseInt(process.env.NEXT_PUBLIC_ENABLE_SESSION_RECORDER || "0"); + + return ( + + + + + + + + {/* Meta info for PWA */} + + + + + + + + + + + + + +
+
+ +
+
{children}
+
+
+ + {process.env.NEXT_PUBLIC_PLAUSIBLE_DOMAIN && ( + + )} + + ); +} diff --git a/dev-wiki/app/not-found.tsx b/dev-wiki/app/not-found.tsx new file mode 100644 index 0000000000..1f1ec0e2c0 --- /dev/null +++ b/dev-wiki/app/not-found.tsx @@ -0,0 +1,46 @@ +"use client"; + +import React from "react"; +import { Metadata } from "next"; +import Image from "next/image"; +import Link from "next/link"; +// ui +import { Button } from "@plane/ui"; +// images +import Image404 from "@/public/404.svg"; + +export const metadata: Metadata = { + title: "404 - Page Not Found", + robots: { + index: false, + follow: false, + }, +}; + +const PageNotFound = () => ( +
+
+
+
+ 404- Page not found +
+
+

Oops! Something went wrong.

+

+ Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is + temporarily unavailable. +

+
+ + + + + +
+
+
+); + +export default PageNotFound; diff --git a/dev-wiki/app/provider.tsx b/dev-wiki/app/provider.tsx new file mode 100644 index 0000000000..bad790d78e --- /dev/null +++ b/dev-wiki/app/provider.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { FC, ReactNode } from "react"; +import dynamic from "next/dynamic"; +import { useTheme, ThemeProvider } from "next-themes"; +import { SWRConfig } from "swr"; +// Plane Imports +import { WEB_SWR_CONFIG } from "@plane/constants"; +import { TranslationProvider } from "@plane/i18n"; +import { Toast } from "@plane/ui"; +//helpers +import { resolveGeneralTheme } from "@/helpers/theme.helper"; +// nprogress +import { AppProgressBar } from "@/lib/n-progress"; +// polyfills +import "@/lib/polyfills"; +// mobx store provider +import { StoreProvider } from "@/lib/store-context"; +// wrappers +import { InstanceWrapper } from "@/lib/wrappers"; +// dynamic imports +const StoreWrapper = dynamic(() => import("@/lib/wrappers/store-wrapper"), { ssr: false }); + +export interface IAppProvider { + children: ReactNode; +} + +const ToastWithTheme = () => { + const { resolvedTheme } = useTheme(); + return ; +}; + +export const AppProvider: FC = (props) => { + const { children } = props; + // themes + return ( + <> + + + + + + + + {children} + + + + + + + ); +}; diff --git a/dev-wiki/ce/components/pages/extra-actions.tsx b/dev-wiki/ce/components/pages/extra-actions.tsx new file mode 100644 index 0000000000..dd15dab7e6 --- /dev/null +++ b/dev-wiki/ce/components/pages/extra-actions.tsx @@ -0,0 +1,8 @@ +// store +import { TPageInstance } from "@/store/pages/base-page"; + +export type TPageHeaderExtraActionsProps = { + page: TPageInstance; +}; + +export const PageDetailsHeaderExtraActions: React.FC = () => null; diff --git a/dev-wiki/ce/layouts/workspace-wrapper.tsx b/dev-wiki/ce/layouts/workspace-wrapper.tsx new file mode 100644 index 0000000000..fcde83e7f3 --- /dev/null +++ b/dev-wiki/ce/layouts/workspace-wrapper.tsx @@ -0,0 +1,15 @@ +import { FC } from "react"; +import { observer } from "mobx-react"; +// layouts +import { WorkspaceAuthWrapper as CoreWorkspaceAuthWrapper } from "@/layouts/auth-layout"; + +export type IWorkspaceAuthWrapper = { + children: React.ReactNode; +}; + +export const WorkspaceAuthWrapper: FC = observer((props) => { + // props + const { children } = props; + + return {children}; +}); diff --git a/dev-wiki/core/components/auth-screens/index.ts b/dev-wiki/core/components/auth-screens/index.ts new file mode 100644 index 0000000000..daff2fb08a --- /dev/null +++ b/dev-wiki/core/components/auth-screens/index.ts @@ -0,0 +1,2 @@ +export * from "./workspace"; +export * from "./not-authorized-view"; diff --git a/dev-wiki/core/components/auth-screens/not-authorized-view.tsx b/dev-wiki/core/components/auth-screens/not-authorized-view.tsx new file mode 100644 index 0000000000..78fe56217f --- /dev/null +++ b/dev-wiki/core/components/auth-screens/not-authorized-view.tsx @@ -0,0 +1,37 @@ +import React from "react"; +import { observer } from "mobx-react"; +import Image from "next/image"; +// layouts +import { cn } from "@plane/utils"; +import DefaultLayout from "@/layouts/default-layout"; +// images +import ProjectNotAuthorizedImg from "@/public/auth/project-not-authorized.svg"; +import Unauthorized from "@/public/auth/unauthorized.svg"; +import WorkspaceNotAuthorizedImg from "@/public/auth/workspace-not-authorized.svg"; + +type Props = { + actionButton?: React.ReactNode; + section?: "settings" | "general"; + isProjectView?: boolean; + className?: string; +}; + +export const NotAuthorizedView: React.FC = observer((props) => { + const { actionButton, section = "general", isProjectView = false, className } = props; + + // assets + const settingAsset = isProjectView ? ProjectNotAuthorizedImg : WorkspaceNotAuthorizedImg; + const asset = section === "settings" ? settingAsset : Unauthorized; + + return ( + +
+
+ ProjectSettingImg +
+

Oops! You are not authorized to view this page

+ {actionButton} +
+
+ ); +}); diff --git a/dev-wiki/core/components/auth-screens/workspace/index.ts b/dev-wiki/core/components/auth-screens/workspace/index.ts new file mode 100644 index 0000000000..8283243122 --- /dev/null +++ b/dev-wiki/core/components/auth-screens/workspace/index.ts @@ -0,0 +1 @@ +export * from "./not-a-member"; diff --git a/dev-wiki/core/components/auth-screens/workspace/not-a-member.tsx b/dev-wiki/core/components/auth-screens/workspace/not-a-member.tsx new file mode 100644 index 0000000000..404709846e --- /dev/null +++ b/dev-wiki/core/components/auth-screens/workspace/not-a-member.tsx @@ -0,0 +1,35 @@ +"use client"; + +import Link from "next/link"; +// ui +import { Button } from "@plane/ui"; +// layouts +import DefaultLayout from "@/layouts/default-layout"; + +export const NotAWorkspaceMember = () => ( + +
+
+
+

Not Authorized!

+

+ You{"'"}re not a member of this workspace. Please contact the workspace admin to get an invitation or check + your pending invitations. +

+
+
+ + + + + + + + + + +
+
+
+
+); diff --git a/dev-wiki/core/components/common/access-field.tsx b/dev-wiki/core/components/common/access-field.tsx new file mode 100644 index 0000000000..f2c50934d2 --- /dev/null +++ b/dev-wiki/core/components/common/access-field.tsx @@ -0,0 +1,53 @@ +import { LucideIcon } from "lucide-react"; +// plane ui +import { useTranslation } from "@plane/i18n"; +import { Tooltip } from "@plane/ui"; +// plane utils +import { cn } from "@plane/utils"; + +type Props = { + onChange: (value: number) => void; + value: number; + accessSpecifiers: { + key: number; + i18n_label?: string; + label?: string; + icon: LucideIcon; + }[]; + isMobile?: boolean; +}; + +// TODO: Remove label once i18n is done +export const AccessField = (props: Props) => { + const { onChange, value, accessSpecifiers, isMobile = false } = props; + const { t } = useTranslation(); + + return ( +
+ {accessSpecifiers.map((access, index) => { + const label = access.i18n_label ? t(access.i18n_label) : access.label; + return ( + + + + ); + })} +
+ ); +}; diff --git a/dev-wiki/core/components/common/activity/activity-block.tsx b/dev-wiki/core/components/common/activity/activity-block.tsx new file mode 100644 index 0000000000..61c1a0b7b4 --- /dev/null +++ b/dev-wiki/core/components/common/activity/activity-block.tsx @@ -0,0 +1,56 @@ +"use client"; + +import { FC, ReactNode } from "react"; +import { Network } from "lucide-react"; +// types +import { TWorkspaceBaseActivity } from "@plane/types"; +// ui +import { Tooltip } from "@plane/ui"; +// helpers +import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "@/helpers/date-time.helper"; +// hooks +import { usePlatformOS } from "@/hooks/use-platform-os"; +// local components +import { User } from "./user"; + +type TActivityBlockComponent = { + icon?: ReactNode; + activity: TWorkspaceBaseActivity; + ends: "top" | "bottom" | undefined; + children: ReactNode; + customUserName?: string; +}; + +export const ActivityBlockComponent: FC = (props) => { + const { icon, activity, ends, children, customUserName } = props; + // hooks + const { isMobile } = usePlatformOS(); + + if (!activity) return <>; + return ( +
+
+ {icon ? icon : } +
+
+
+ {children} +
+
+ + + {calculateTimeAgo(activity.created_at)} + + +
+
+
+ ); +}; diff --git a/dev-wiki/core/components/common/activity/activity-item.tsx b/dev-wiki/core/components/common/activity/activity-item.tsx new file mode 100644 index 0000000000..643d897383 --- /dev/null +++ b/dev-wiki/core/components/common/activity/activity-item.tsx @@ -0,0 +1,32 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; + +import { TProjectActivity } from "@/plane-web/types"; +import { ActivityBlockComponent } from "./activity-block"; +import { iconsMap, messages } from "./helper"; + +type TActivityItem = { + activity: TProjectActivity; + showProject?: boolean; + ends?: "top" | "bottom" | undefined; +}; + +export const ActivityItem: FC = observer((props) => { + const { activity, showProject = true, ends } = props; + + if (!activity) return null; + + const activityType = activity.field; + if (!activityType) return null; + + const { message, customUserName } = messages(activity); + const icon = iconsMap[activityType] || iconsMap.default; + + return ( + + <>{message} + + ); +}); diff --git a/dev-wiki/core/components/common/activity/helper.tsx b/dev-wiki/core/components/common/activity/helper.tsx new file mode 100644 index 0000000000..b3d16e7d04 --- /dev/null +++ b/dev-wiki/core/components/common/activity/helper.tsx @@ -0,0 +1,281 @@ +import { ReactNode } from "react"; +import { + Signal, + RotateCcw, + Network, + Link as LinkIcon, + Calendar, + Tag, + Inbox, + AlignLeft, + Users, + Paperclip, + Type, + Triangle, + FileText, + Globe, + Hash, + Clock, + Bell, + LayoutGrid, + GitBranch, + Timer, + ListTodo, + Layers, +} from "lucide-react"; + +// components +import { ArchiveIcon, DoubleCircleIcon, ContrastIcon, DiceIcon, Intake } from "@plane/ui"; +import { store } from "@/lib/store-context"; +import { TProjectActivity } from "@/plane-web/types"; + +type ActivityIconMap = { + [key: string]: ReactNode; +}; +export const iconsMap: ActivityIconMap = { + priority: , + archived_at: , + restored: , + link: , + start_date: , + target_date: , + label: , + inbox: , + description: , + assignee: , + attachment: , + name: , + state: , + estimate: , + cycle: , + module: , + page: , + network: , + identifier: , + timezone: , + is_project_updates_enabled: , + is_epic_enabled: , + is_workflow_enabled: , + is_time_tracking_enabled: , + is_issue_type_enabled: , + default: , + module_view: , + cycle_view: , + issue_views_view: , + page_view: , + intake_view: , +}; + +export const messages = (activity: TProjectActivity): { message: string | ReactNode; customUserName?: string } => { + const activityType = activity.field; + const newValue = activity.new_value; + const oldValue = activity.old_value; + const verb = activity.verb; + const workspaceDetail = store.workspaceRoot.getWorkspaceById(activity.workspace); + + const getBooleanActionText = (value: string | undefined) => { + if (value === "true") return "enabled"; + if (value === "false") return "disabled"; + return verb; + }; + + switch (activityType) { + case "priority": + return { + message: ( + <> + set the priority to {newValue || "none"} + + ), + }; + case "archived_at": + return { + message: newValue === "restore" ? "restored the project" : "archived the project", + customUserName: newValue === "archive" ? "Plane" : undefined, + }; + case "name": + return { + message: ( + <> + renamed the project to {newValue} + + ), + }; + case "description": + return { + message: newValue ? "updated the project description" : "removed the project description", + }; + case "start_date": + return { + message: ( + <> + {newValue ? ( + <> + set the start date to {newValue} + + ) : ( + "removed the start date" + )} + + ), + }; + case "target_date": + return { + message: ( + <> + {newValue ? ( + <> + set the target date to {newValue} + + ) : ( + "removed the target date" + )} + + ), + }; + case "state": + return { + message: ( + <> + set the state to {newValue || "none"} + + ), + }; + case "estimate": + return { + message: ( + <> + {newValue ? ( + <> + set the estimate point to {newValue} + + ) : ( + <> + removed the estimate point + {oldValue && ( + <> + {" "} + {oldValue} + + )} + + )} + + ), + }; + case "cycles": + return { + message: ( + <> + + {verb} this project {verb === "removed" ? "from" : "to"} the cycle{" "} + + {verb !== "removed" ? ( + + {activity.new_value} + + ) : ( + {activity.old_value || "Unknown cycle"} + )} + + ), + }; + case "modules": + return { + message: ( + <> + + {verb} this project {verb === "removed" ? "from" : "to"} the module{" "} + + + {verb === "removed" ? oldValue : newValue || "Unknown module"} + + + ), + }; + case "labels": + return { + message: ( + <> + {verb} the label{" "} + {newValue || oldValue || "Untitled label"} + + ), + }; + case "inbox": + return { + message: <>{newValue ? "enabled" : "disabled"} inbox, + }; + case "page": + return { + message: ( + <> + {newValue ? "created" : "removed"} the project page{" "} + {newValue || oldValue || "Untitled page"} + + ), + }; + case "network": + return { + message: <>{newValue ? "enabled" : "disabled"} network access, + }; + case "identifier": + return { + message: ( + <> + updated project identifier to {newValue || "none"} + + ), + }; + case "timezone": + return { + message: ( + <> + changed project timezone to{" "} + {newValue || "default"} + + ), + }; + case "module_view": + case "cycle_view": + case "issue_views_view": + case "page_view": + case "intake_view": + return { + message: ( + <> + {getBooleanActionText(newValue)} {activityType.replace(/_view$/, "").replace(/_/g, " ")} view + + ), + }; + case "is_project_updates_enabled": + return { + message: <>{getBooleanActionText(newValue)} project updates, + }; + case "is_epic_enabled": + return { + message: <>{getBooleanActionText(newValue)} epics, + }; + case "is_workflow_enabled": + return { + message: <>{getBooleanActionText(newValue)} custom workflow, + }; + case "is_time_tracking_enabled": + return { + message: <>{getBooleanActionText(newValue)} time tracking, + }; + case "is_issue_type_enabled": + return { + message: <>{getBooleanActionText(newValue)} work item types, + }; + default: + return { + message: `${verb} ${activityType?.replace(/_/g, " ")} `, + }; + } +}; diff --git a/dev-wiki/core/components/common/activity/index.ts b/dev-wiki/core/components/common/activity/index.ts new file mode 100644 index 0000000000..8ab32d3d90 --- /dev/null +++ b/dev-wiki/core/components/common/activity/index.ts @@ -0,0 +1 @@ +export * from "./activity-item"; diff --git a/dev-wiki/core/components/common/activity/user.tsx b/dev-wiki/core/components/common/activity/user.tsx new file mode 100644 index 0000000000..c82ab36162 --- /dev/null +++ b/dev-wiki/core/components/common/activity/user.tsx @@ -0,0 +1,37 @@ +import { FC } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +// types +import { TWorkspaceBaseActivity } from "@plane/types"; +// store hooks +import { useMember, useWorkspace } from "@/hooks/store"; + +type TUser = { + activity: TWorkspaceBaseActivity; + customUserName?: string; +}; + +export const User: FC = observer((props) => { + const { activity, customUserName } = props; + // store hooks + const { getUserDetails } = useMember(); + const { getWorkspaceById } = useWorkspace(); + // derived values + const actorDetail = getUserDetails(activity.actor); + const workspaceDetail = getWorkspaceById(activity.workspace); + + return ( + <> + {customUserName || actorDetail?.display_name.includes("-intake") ? ( + {customUserName || "Plane"} + ) : ( + + {actorDetail?.display_name} + + )} + + ); +}); diff --git a/dev-wiki/core/components/common/applied-filters/date.tsx b/dev-wiki/core/components/common/applied-filters/date.tsx new file mode 100644 index 0000000000..9dcde53a43 --- /dev/null +++ b/dev-wiki/core/components/common/applied-filters/date.tsx @@ -0,0 +1,56 @@ +import { observer } from "mobx-react"; +// icons +import { X } from "lucide-react"; +// plane constants +import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants"; +// helpers +import { renderFormattedDate } from "@/helpers/date-time.helper"; +import { capitalizeFirstLetter } from "@/helpers/string.helper"; + +type Props = { + editable: boolean | undefined; + handleRemove: (val: string) => void; + values: string[]; +}; + +export const AppliedDateFilters: React.FC = observer((props) => { + const { editable, handleRemove, values } = props; + + const getDateLabel = (value: string): string => { + let dateLabel = ""; + + const dateDetails = DATE_BEFORE_FILTER_OPTIONS.find((d) => d.value === value); + + if (dateDetails) dateLabel = dateDetails.name; + else { + const dateParts = value.split(";"); + + if (dateParts.length === 2) { + const [date, time] = dateParts; + + dateLabel = `${capitalizeFirstLetter(time)} ${renderFormattedDate(date)}`; + } + } + + return dateLabel; + }; + + return ( + <> + {values.map((date) => ( +
+ {getDateLabel(date)} + {editable && ( + + )} +
+ ))} + + ); +}); diff --git a/dev-wiki/core/components/common/applied-filters/index.ts b/dev-wiki/core/components/common/applied-filters/index.ts new file mode 100644 index 0000000000..77f9f6304c --- /dev/null +++ b/dev-wiki/core/components/common/applied-filters/index.ts @@ -0,0 +1,2 @@ +export * from "./date"; +export * from "./members"; diff --git a/dev-wiki/core/components/common/applied-filters/members.tsx b/dev-wiki/core/components/common/applied-filters/members.tsx new file mode 100644 index 0000000000..87d3393666 --- /dev/null +++ b/dev-wiki/core/components/common/applied-filters/members.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { observer } from "mobx-react"; +import { X } from "lucide-react"; +// plane ui +import { Avatar } from "@plane/ui"; +// helpers +import { getFileURL } from "@/helpers/file.helper"; +// types +import { useMember } from "@/hooks/store"; + +type Props = { + handleRemove: (val: string) => void; + values: string[]; + editable: boolean | undefined; +}; + +export const AppliedMembersFilters: React.FC = observer((props) => { + const { handleRemove, values, editable } = props; + // store hooks + const { + workspace: { getWorkspaceMemberDetails }, + } = useMember(); + + return ( + <> + {values.map((memberId) => { + const memberDetails = getWorkspaceMemberDetails(memberId)?.member; + + if (!memberDetails) return null; + + return ( +
+ + {memberDetails.display_name} + {editable && ( + + )} +
+ ); + })} + + ); +}); diff --git a/dev-wiki/core/components/common/breadcrumb-link.tsx b/dev-wiki/core/components/common/breadcrumb-link.tsx new file mode 100644 index 0000000000..bf421a50c2 --- /dev/null +++ b/dev-wiki/core/components/common/breadcrumb-link.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { ReactNode } from "react"; +import Link from "next/link"; +import { Tooltip } from "@plane/ui"; +import { usePlatformOS } from "@/hooks/use-platform-os"; + +type Props = { + label?: string | ReactNode; + href?: string; + icon?: React.ReactNode | undefined; + disableTooltip?: boolean; +}; + +export const BreadcrumbLink: React.FC = (props) => { + const { href, label, icon, disableTooltip = false } = props; + const { isMobile } = usePlatformOS(); + return ( + +
  • +
    + {href ? ( + + {icon && ( +
    {icon}
    + )} + {label && ( +
    {label}
    + )} + + ) : ( +
    + {icon &&
    {icon}
    } +
    {label}
    +
    + )} +
    +
  • +
    + ); +}; diff --git a/dev-wiki/core/components/common/count-chip.tsx b/dev-wiki/core/components/common/count-chip.tsx new file mode 100644 index 0000000000..0b5820d75a --- /dev/null +++ b/dev-wiki/core/components/common/count-chip.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { FC } from "react"; +// +import { cn } from "@/helpers/common.helper"; + +type TCountChip = { + count: string | number; + className?: string; +}; + +export const CountChip: FC = (props) => { + const { count, className = "" } = props; + + return ( +
    + {count} +
    + ); +}; diff --git a/dev-wiki/core/components/common/custom-error.tsx b/dev-wiki/core/components/common/custom-error.tsx new file mode 100644 index 0000000000..1b82b08afb --- /dev/null +++ b/dev-wiki/core/components/common/custom-error.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { Button } from "@plane/ui"; + +export const CustomError = () => { + const handleRefresh = () => { + window.location.reload(); + }; + + return ( +
    +
    +
    +
    +

    Yikes! That doesn{"'"}t look good.

    +

    + That crashed Plane, pun intended. No worries, though. Our engineers have been notified. If you have more + details, please write to{" "} + + support@plane.so + {" "} + or on our{" "} + + Discord + + . +

    +
    +
    + +
    +
    +
    +
    + ); +}; diff --git a/dev-wiki/core/components/common/empty-state.tsx b/dev-wiki/core/components/common/empty-state.tsx new file mode 100644 index 0000000000..5fdd210d91 --- /dev/null +++ b/dev-wiki/core/components/common/empty-state.tsx @@ -0,0 +1,50 @@ +"use client"; +import React from "react"; + +import Image from "next/image"; + +// ui +import { Button } from "@plane/ui"; + +type Props = { + title: string; + description?: React.ReactNode; + image: any; + primaryButton?: { + icon?: any; + text: string; + onClick: () => void; + }; + secondaryButton?: React.ReactNode; + disabled?: boolean; +}; + +export const EmptyState: React.FC = ({ + title, + description, + image, + primaryButton, + secondaryButton, + disabled = false, +}) => ( +
    +
    + {primaryButton?.text +
    {title}
    + {description &&

    {description}

    } +
    + {primaryButton && ( + + )} + {secondaryButton} +
    +
    +
    +); diff --git a/dev-wiki/core/components/common/filters/created-at.tsx b/dev-wiki/core/components/common/filters/created-at.tsx new file mode 100644 index 0000000000..8a43a4985d --- /dev/null +++ b/dev-wiki/core/components/common/filters/created-at.tsx @@ -0,0 +1,76 @@ +import React, { useState } from "react"; +import { observer } from "mobx-react"; +// plane constants +import { DATE_BEFORE_FILTER_OPTIONS } from "@plane/constants"; +import { DateFilterModal } from "@/components/core"; +// import { FilterHeader, FilterOption } from "@/components/issues"; +// helpers +import { isInDateFormat } from "@/helpers/date-time.helper"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string | string[]) => void; + searchQuery: string; +}; + +export const FilterCreatedDate: React.FC = observer((props) => { + const { appliedFilters, handleUpdate, searchQuery } = props; + + const [previewEnabled, setPreviewEnabled] = useState(true); + const [isDateFilterModalOpen, setIsDateFilterModalOpen] = useState(false); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const filteredOptions = DATE_BEFORE_FILTER_OPTIONS.filter((d) => + d.name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + const isCustomDateSelected = () => { + const isValidDateSelected = appliedFilters?.filter((f) => isInDateFormat(f.split(";")[0])) || []; + return isValidDateSelected.length > 0 ? true : false; + }; + const handleCustomDate = () => { + if (isCustomDateSelected()) { + const updateAppliedFilters = appliedFilters?.filter((f) => f.includes("-")) || []; + handleUpdate(updateAppliedFilters); + } else setIsDateFilterModalOpen(true); + }; + + return ( + <> + {isDateFilterModalOpen && ( + setIsDateFilterModalOpen(false)} + isOpen={isDateFilterModalOpen} + onSelect={(val) => handleUpdate(val)} + title="Created date" + /> + )} + {/* 0 ? ` (${appliedFiltersCount})` : ""}`} */} + {/* isPreviewEnabled={previewEnabled} */} + {/* handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} */} + {/* /> */} + {/* {previewEnabled && ( */} + {/*
    */} + {/* {filteredOptions.length > 0 ? ( */} + {/* <> */} + {/* {filteredOptions.map((option) => ( */} + {/* handleUpdate(option.value)} */} + {/* title={option.name} */} + {/* multiple */} + {/* /> */} + {/* ))} */} + {/* */} + {/* */} + {/* ) : ( */} + {/*

    No matches found

    */} + {/* )} */} + {/*
    */} + {/* )} */} + + ); +}); diff --git a/dev-wiki/core/components/common/filters/created-by.tsx b/dev-wiki/core/components/common/filters/created-by.tsx new file mode 100644 index 0000000000..c1476d28ab --- /dev/null +++ b/dev-wiki/core/components/common/filters/created-by.tsx @@ -0,0 +1,110 @@ +"use client"; + +import { useMemo, useState } from "react"; +import sortBy from "lodash/sortBy"; +import { observer } from "mobx-react"; +// ui +import { Avatar, Loader } from "@plane/ui"; +// components +// import { FilterHeader, FilterOption } from "@/components/issues"; +// helpers +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useMember, useUser } from "@/hooks/store"; + +type Props = { + appliedFilters: string[] | null; + handleUpdate: (val: string) => void; + memberIds: string[] | undefined; + searchQuery: string; +}; + +export const FilterCreatedBy: React.FC = observer((props: Props) => { + const { appliedFilters, handleUpdate, memberIds, searchQuery } = props; + // states + const [itemsToRender, setItemsToRender] = useState(5); + const [previewEnabled, setPreviewEnabled] = useState(true); + // store hooks + const { getUserDetails } = useMember(); + const { data: currentUser } = useUser(); + + const appliedFiltersCount = appliedFilters?.length ?? 0; + + const sortedOptions = useMemo(() => { + const filteredOptions = (memberIds || []).filter((memberId) => + getUserDetails(memberId)?.display_name.toLowerCase().includes(searchQuery.toLowerCase()) + ); + + return sortBy(filteredOptions, [ + (memberId) => !(appliedFilters ?? []).includes(memberId), + (memberId) => memberId !== currentUser?.id, + (memberId) => getUserDetails(memberId)?.display_name.toLowerCase(), + ]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchQuery]); + + const handleViewToggle = () => { + if (!sortedOptions) return; + + if (itemsToRender === sortedOptions.length) setItemsToRender(5); + else setItemsToRender(sortedOptions.length); + }; + + return ( + <> + {/* 0 ? ` (${appliedFiltersCount})` : ""}`} */} + {/* isPreviewEnabled={previewEnabled} */} + {/* handleIsPreviewEnabled={() => setPreviewEnabled(!previewEnabled)} */} + {/* /> */} + {/* {previewEnabled && ( */} + {/*
    */} + {/* {sortedOptions ? ( */} + {/* sortedOptions.length > 0 ? ( */} + {/* <> */} + {/* {sortedOptions.slice(0, itemsToRender).map((memberId) => { */} + {/* const member = getUserDetails(memberId); */} + {/**/} + {/* if (!member) return null; */} + {/* return ( */} + {/* handleUpdate(member.id)} */} + {/* icon={ */} + {/* */} + {/* } */} + {/* title={currentUser?.id === member.id ? "You" : member?.display_name} */} + {/* /> */} + {/* ); */} + {/* })} */} + {/* {sortedOptions.length > 5 && ( */} + {/* */} + {/* )} */} + {/* */} + {/* ) : ( */} + {/*

    No matches found

    */} + {/* ) */} + {/* ) : ( */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* )} */} + {/*
    */} + {/* )} */} + + ); +}); diff --git a/dev-wiki/core/components/common/filters/index.ts b/dev-wiki/core/components/common/filters/index.ts new file mode 100644 index 0000000000..6f0b5dbdef --- /dev/null +++ b/dev-wiki/core/components/common/filters/index.ts @@ -0,0 +1,2 @@ +export * from "./created-at"; +export * from "./created-by"; diff --git a/dev-wiki/core/components/common/index.ts b/dev-wiki/core/components/common/index.ts new file mode 100644 index 0000000000..5a9aa4c34b --- /dev/null +++ b/dev-wiki/core/components/common/index.ts @@ -0,0 +1,11 @@ +export * from "./empty-state"; +export * from "./latest-feature-block"; +export * from "./breadcrumb-link"; +export * from "./logo-spinner"; +export * from "./logo"; +export * from "./pro-icon"; +export * from "./custom-error"; +export * from "./count-chip"; +export * from "./activity"; +export * from "./switcher-label"; +export * from "./page-access-icon"; diff --git a/dev-wiki/core/components/common/latest-feature-block.tsx b/dev-wiki/core/components/common/latest-feature-block.tsx new file mode 100644 index 0000000000..35aacddcfe --- /dev/null +++ b/dev-wiki/core/components/common/latest-feature-block.tsx @@ -0,0 +1,40 @@ +import Image from "next/image"; +import Link from "next/link"; +import { useTheme } from "next-themes"; +// icons +import { Lightbulb } from "lucide-react"; +// images +import latestFeatures from "@/public/onboarding/onboarding-pages.webp"; + +export const LatestFeatureBlock = () => { + const { resolvedTheme } = useTheme(); + + return ( + <> +
    + +

    + Pages gets a facelift! Write anything and use Galileo to help you start.{" "} + + Learn more + +

    +
    +
    +
    + Plane Work items +
    +
    + + ); +}; diff --git a/dev-wiki/core/components/common/logo-spinner.tsx b/dev-wiki/core/components/common/logo-spinner.tsx new file mode 100644 index 0000000000..abcebfe76c --- /dev/null +++ b/dev-wiki/core/components/common/logo-spinner.tsx @@ -0,0 +1,17 @@ +import Image from "next/image"; +import { useTheme } from "next-themes"; +// assets +import LogoSpinnerDark from "@/public/images/logo-spinner-dark.gif"; +import LogoSpinnerLight from "@/public/images/logo-spinner-light.gif"; + +export const LogoSpinner = () => { + const { resolvedTheme } = useTheme(); + + const logoSrc = resolvedTheme === "dark" ? LogoSpinnerDark : LogoSpinnerLight; + + return ( +
    + logo +
    + ); +}; diff --git a/dev-wiki/core/components/common/logo.tsx b/dev-wiki/core/components/common/logo.tsx new file mode 100644 index 0000000000..49d4d60a12 --- /dev/null +++ b/dev-wiki/core/components/common/logo.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { FC } from "react"; +import { Emoji } from "emoji-picker-react"; +// Due to some weird issue with the import order, the import of useFontFaceObserver +// should be after the imported here rather than some below helper functions as it is in the original file +// eslint-disable-next-line import/order +import useFontFaceObserver from "use-font-face-observer"; +// types +import { TLogoProps } from "@plane/types"; +// ui +import { LUCIDE_ICONS_LIST } from "@plane/ui"; +// helpers +import { emojiCodeToUnicode } from "@/helpers/emoji.helper"; + +type Props = { + logo: TLogoProps; + size?: number; + type?: "lucide" | "material"; +}; + +export const Logo: FC = (props) => { + const { logo, size = 16, type = "material" } = props; + + // destructuring the logo object + const { in_use, emoji, icon } = logo; + + // derived values + const value = in_use === "emoji" ? emoji?.value : icon?.name; + const color = icon?.color; + const lucideIcon = LUCIDE_ICONS_LIST.find((item) => item.name === value); + + const isMaterialSymbolsFontLoaded = useFontFaceObserver([ + { + family: `Material Symbols Rounded`, + style: `normal`, + weight: `normal`, + stretch: `condensed`, + }, + ]); + // if no value, return empty fragment + if (!value) return <>; + + if (!isMaterialSymbolsFontLoaded) { + return ( + + ); + } + + // emoji + if (in_use === "emoji") { + return ; + } + + // icon + if (in_use === "icon") { + return ( + <> + {type === "lucide" ? ( + <> + {lucideIcon && ( + + )} + + ) : ( + + {value} + + )} + + ); + } + + // if no value, return empty fragment + return <>; +}; diff --git a/dev-wiki/core/components/common/new-empty-state.tsx b/dev-wiki/core/components/common/new-empty-state.tsx new file mode 100644 index 0000000000..e04cb3ed05 --- /dev/null +++ b/dev-wiki/core/components/common/new-empty-state.tsx @@ -0,0 +1,114 @@ +"use client"; +import React, { useState } from "react"; + +import Image from "next/image"; + +// ui +import { Button } from "@plane/ui"; + +type Props = { + title: string; + description?: React.ReactNode; + image: any; + comicBox?: { + direction: "left" | "right"; + title: string; + description: string; + extraPadding?: boolean; + }; + primaryButton?: { + icon?: any; + text: string; + onClick: () => void; + }; + disabled?: boolean; +}; + +export const NewEmptyState: React.FC = ({ + title, + description, + image, + primaryButton, + disabled = false, + comicBox, +}) => { + const [isHovered, setIsHovered] = useState(false); + + const handleMouseEnter = () => { + setIsHovered(true); + }; + + const handleMouseLeave = () => { + setIsHovered(false); + }; + return ( +
    +
    +
    +

    {title}

    + {description &&

    {description}

    } +
    + {primaryButton?.text +
    + +
    + {primaryButton && ( + + )} + {comicBox && + isHovered && + (comicBox.direction === "right" ? ( +
    +
    +
    +
    +
    +

    +

    {comicBox?.title}

    +

    {comicBox?.description}

    + +
    +
    + ) : ( +
    +
    +
    +
    +
    +

    +

    {comicBox?.title}

    +

    {comicBox?.description}

    + +
    +
    + ))} +
    +
    +
    +
    + ); +}; diff --git a/dev-wiki/core/components/common/page-access-icon.tsx b/dev-wiki/core/components/common/page-access-icon.tsx new file mode 100644 index 0000000000..9f547a1163 --- /dev/null +++ b/dev-wiki/core/components/common/page-access-icon.tsx @@ -0,0 +1,15 @@ +import { ArchiveIcon, Earth, Lock } from "lucide-react"; +import { EPageAccess } from "@plane/constants"; +import { TPage } from "@plane/types"; + +export const PageAccessIcon = (page: TPage) => ( +
    + {page.archived_at ? ( + + ) : page.access === EPageAccess.PUBLIC ? ( + + ) : ( + + )} +
    +); diff --git a/dev-wiki/core/components/common/pro-icon.tsx b/dev-wiki/core/components/common/pro-icon.tsx new file mode 100644 index 0000000000..39d4c2a901 --- /dev/null +++ b/dev-wiki/core/components/common/pro-icon.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { FC } from "react"; +import { Crown } from "lucide-react"; +// helpers +import { cn } from "@/helpers/common.helper"; + +type TProIcon = { + className?: string; +}; + +export const ProIcon: FC = (props) => { + const { className } = props; + + return ; +}; diff --git a/dev-wiki/core/components/common/switcher-label.tsx b/dev-wiki/core/components/common/switcher-label.tsx new file mode 100644 index 0000000000..e1426461c9 --- /dev/null +++ b/dev-wiki/core/components/common/switcher-label.tsx @@ -0,0 +1,27 @@ +import { FC } from "react"; +import { TLogoProps } from "@plane/types"; +import { ISvgIcons, Logo } from "@plane/ui"; +import { getFileURL } from "@plane/utils"; +import { truncateText } from "@/helpers/string.helper"; +type TSwitcherLabelProps = { + logo_props?: TLogoProps; + logo_url?: string; + name?: string; + LabelIcon: FC; +}; + +export const SwitcherLabel: FC = (props) => { + const { logo_props, name, LabelIcon, logo_url } = props; + return ( +
    + {logo_props?.in_use ? ( + + ) : logo_url ? ( + logo + ) : ( + + )} + {truncateText(name ?? "", 40)} +
    + ); +}; diff --git a/dev-wiki/core/components/core/app-header.tsx b/dev-wiki/core/components/core/app-header.tsx new file mode 100644 index 0000000000..8320bd9f71 --- /dev/null +++ b/dev-wiki/core/components/core/app-header.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { ReactNode } from "react"; +// components +import { Row } from "@plane/ui"; +import { SidebarHamburgerToggle } from "@/components/core"; +// plane web components + +export interface AppHeaderProps { + header: ReactNode; + mobileHeader?: ReactNode; +} + +export const AppHeader = (props: AppHeaderProps) => { + const { header, mobileHeader } = props; + + return ( +
    + +
    + +
    +
    {header}
    +
    + {mobileHeader && mobileHeader} +
    + ); +}; diff --git a/dev-wiki/core/components/core/content-overflow-HOC.tsx b/dev-wiki/core/components/core/content-overflow-HOC.tsx new file mode 100644 index 0000000000..f8089c34bc --- /dev/null +++ b/dev-wiki/core/components/core/content-overflow-HOC.tsx @@ -0,0 +1,156 @@ +import { ReactNode, useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { useTranslation } from "@plane/i18n"; +import { cn } from "@plane/utils"; + +interface IContentOverflowWrapper { + children: ReactNode; + maxHeight?: number; + gradientColor?: string; + buttonClassName?: string; + containerClassName?: string; + fallback?: ReactNode; + customButton?: ReactNode; +} + +export const ContentOverflowWrapper = observer((props: IContentOverflowWrapper) => { + const { + children, + maxHeight = 625, + buttonClassName = "text-sm font-medium text-custom-primary-100", + containerClassName, + fallback = null, + customButton, + } = props; + + // states + const [containerHeight, setContainerHeight] = useState(0); + const [showAll, setShowAll] = useState(false); + const [isTransitioning, setIsTransitioning] = useState(false); + + // refs + const contentRef = useRef(null); + const containerRef = useRef(null); + + // hooks + const { t } = useTranslation(); + + useEffect(() => { + if (!contentRef?.current) return; + + const updateHeight = () => { + if (contentRef.current) { + const height = contentRef.current.getBoundingClientRect().height; + setContainerHeight(height); + } + }; + + // Initial height measurement + updateHeight(); + + // Create ResizeObserver for size changes + const resizeObserver = new ResizeObserver(updateHeight); + resizeObserver.observe(contentRef.current); + + // Create MutationObserver for content changes + const mutationObserver = new MutationObserver((mutations) => { + const shouldUpdate = mutations.some( + (mutation) => + mutation.type === "childList" || + (mutation.type === "attributes" && (mutation.attributeName === "style" || mutation.attributeName === "class")) + ); + + if (shouldUpdate) { + updateHeight(); + } + }); + + mutationObserver.observe(contentRef.current, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["style", "class"], + }); + + return () => { + resizeObserver.disconnect(); + mutationObserver.disconnect(); + }; + }, [contentRef?.current]); + + useEffect(() => { + if (!containerRef.current) return; + + const handleTransitionEnd = () => { + setIsTransitioning(false); + }; + + containerRef.current.addEventListener("transitionend", handleTransitionEnd); + + return () => { + containerRef.current?.removeEventListener("transitionend", handleTransitionEnd); + }; + }, []); + + const handleToggle = () => { + setIsTransitioning(true); + setShowAll((prev) => !prev); + }; + + if (!children) return fallback; + + return ( +
    maxHeight, + }, + containerClassName + )} + style={{ + height: showAll ? `${containerHeight}px` : `${Math.min(maxHeight, containerHeight)}px`, + }} + > +
    + {children} +
    + + {containerHeight > maxHeight && ( +
    + {customButton || ( + + )} +
    + )} +
    + ); +}); diff --git a/dev-wiki/core/components/core/content-wrapper.tsx b/dev-wiki/core/components/core/content-wrapper.tsx new file mode 100644 index 0000000000..eefc96b1e0 --- /dev/null +++ b/dev-wiki/core/components/core/content-wrapper.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { ReactNode } from "react"; +// helpers +import { cn } from "@/helpers/common.helper"; + +export interface ContentWrapperProps { + className?: string; + children: ReactNode; +} + +export const ContentWrapper = ({ className, children }: ContentWrapperProps) => ( +
    +
    {children}
    +
    +); diff --git a/dev-wiki/core/components/core/filters/date-filter-modal.tsx b/dev-wiki/core/components/core/filters/date-filter-modal.tsx new file mode 100644 index 0000000000..a1c76da4b4 --- /dev/null +++ b/dev-wiki/core/components/core/filters/date-filter-modal.tsx @@ -0,0 +1,170 @@ +"use client"; +import { Fragment } from "react"; +import { Controller, useForm } from "react-hook-form"; + +import { X } from "lucide-react"; +import { Dialog, Transition } from "@headlessui/react"; + +import { Button, Calendar } from "@plane/ui"; + +import { renderFormattedPayloadDate, renderFormattedDate, getDate } from "@/helpers/date-time.helper"; +import { DateFilterSelect } from "./date-filter-select"; +type Props = { + title: string; + handleClose: () => void; + isOpen: boolean; + onSelect: (val: string[]) => void; +}; + +type TFormValues = { + filterType: "before" | "after" | "range"; + date1: Date; + date2: Date; +}; + +const defaultValues: TFormValues = { + filterType: "range", + date1: new Date(), + date2: new Date(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()), +}; + +export const DateFilterModal: React.FC = ({ title, handleClose, isOpen, onSelect }) => { + const { handleSubmit, watch, control } = useForm({ + defaultValues, + }); + + const handleFormSubmit = (formData: TFormValues) => { + const { filterType, date1, date2 } = formData; + + if (filterType === "range") + onSelect([`${renderFormattedPayloadDate(date1)};after`, `${renderFormattedPayloadDate(date2)};before`]); + else onSelect([`${renderFormattedPayloadDate(date1)};${filterType}`]); + + handleClose(); + }; + + const date1 = getDate(watch("date1")); + const date2 = getDate(watch("date1")); + + const isInvalid = watch("filterType") === "range" && date1 && date2 ? date1 > date2 : false; + + return ( + + + +
    + +
    +
    + + +
    +
    + ( + + )} + /> + +
    +
    + { + const dateValue = getDate(value); + const date2Value = getDate(watch("date2")); + return ( + { + if (!date) return; + onChange(date); + }} + mode="single" + disabled={date2Value ? [{ after: date2Value }] : undefined} + /> + ); + }} + /> + {watch("filterType") === "range" && ( + { + const dateValue = getDate(value); + const date1Value = getDate(watch("date1")); + return ( + { + if (!date) return; + onChange(date); + }} + mode="single" + disabled={date1Value ? [{ before: date1Value }] : undefined} + /> + ); + }} + /> + )} +
    + {watch("filterType") === "range" && ( +
    + After: + {renderFormattedDate(watch("date1"))} + Before: + {!isInvalid && {renderFormattedDate(watch("date2"))}} +
    + )} +
    + + +
    +
    +
    +
    +
    +
    +
    +
    + ); +}; diff --git a/dev-wiki/core/components/core/filters/date-filter-select.tsx b/dev-wiki/core/components/core/filters/date-filter-select.tsx new file mode 100644 index 0000000000..45c38c1c30 --- /dev/null +++ b/dev-wiki/core/components/core/filters/date-filter-select.tsx @@ -0,0 +1,59 @@ +"use client"; +import React from "react"; +import { CalendarDays } from "lucide-react"; +// ui +import { CustomSelect, CalendarAfterIcon, CalendarBeforeIcon } from "@plane/ui"; + +type Props = { + title: string; + value: string; + onChange: (value: string) => void; +}; + +type DueDate = { + name: string; + value: string; + icon: any; +}; + +const dueDateRange: DueDate[] = [ + { + name: "before", + value: "before", + icon: , + }, + { + name: "after", + value: "after", + icon: , + }, + { + name: "range", + value: "range", + icon: , + }, +]; + +export const DateFilterSelect: React.FC = ({ title, value, onChange }) => ( + + {dueDateRange.find((item) => item.value === value)?.icon} + + {title} {dueDateRange.find((item) => item.value === value)?.name} + +
    + } + onChange={onChange} + > + {dueDateRange.map((option, index) => ( + +
    + {option.icon} + {title} {option.name} +
    +
    + ))} + +); diff --git a/dev-wiki/core/components/core/filters/index.ts b/dev-wiki/core/components/core/filters/index.ts new file mode 100644 index 0000000000..28e36d3e67 --- /dev/null +++ b/dev-wiki/core/components/core/filters/index.ts @@ -0,0 +1,2 @@ +export * from "./date-filter-modal"; +export * from "./date-filter-select"; diff --git a/dev-wiki/core/components/core/image-picker-popover.tsx b/dev-wiki/core/components/core/image-picker-popover.tsx new file mode 100644 index 0000000000..bade076d9b --- /dev/null +++ b/dev-wiki/core/components/core/image-picker-popover.tsx @@ -0,0 +1,389 @@ +"use client"; + +import React, { useState, useRef, useCallback } from "react"; +import { observer } from "mobx-react"; +import Image from "next/image"; +import { useParams } from "next/navigation"; +import { useDropzone } from "react-dropzone"; +import { Control, Controller } from "react-hook-form"; +import useSWR from "swr"; +import { Tab, Popover } from "@headlessui/react"; +// plane imports +import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants"; +import { useOutsideClickDetector } from "@plane/hooks"; +// plane types +import { EFileAssetType } from "@plane/types/src/enums"; +// ui +import { Button, Input, Loader, TOAST_TYPE, setToast } from "@plane/ui"; +// helpers +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down"; +// services +import { FileService } from "@/services/file.service"; + +const tabOptions = [ + { + key: "unsplash", + title: "Unsplash", + }, + { + key: "images", + title: "Images", + }, + { + key: "upload", + title: "Upload", + }, +]; + +type Props = { + label: string | React.ReactNode; + value: string | null; + control: Control; + onChange: (data: string) => void; + disabled?: boolean; + tabIndex?: number; + isProfileCover?: boolean; + projectId?: string | null; +}; + +// services +const fileService = new FileService(); + +export const ImagePickerPopover: React.FC = observer((props) => { + const { label, value, control, onChange, disabled = false, tabIndex, isProfileCover = false, projectId } = props; + // states + const [image, setImage] = useState(null); + const [isImageUploading, setIsImageUploading] = useState(false); + const [isOpen, setIsOpen] = useState(false); + const [searchParams, setSearchParams] = useState(""); + const [formData, setFormData] = useState({ + search: "", + }); + // refs + const ref = useRef(null); + // router params + const { workspaceSlug } = useParams(); + + const { data: unsplashImages, error: unsplashError } = useSWR( + `UNSPLASH_IMAGES_${searchParams}`, + () => fileService.getUnsplashImages(searchParams), + { + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ); + + const { data: projectCoverImages } = useSWR(`PROJECT_COVER_IMAGES`, () => fileService.getProjectCoverImages(), { + revalidateOnFocus: false, + revalidateOnReconnect: false, + }); + + const imagePickerRef = useRef(null); + + const onDrop = useCallback((acceptedFiles: File[]) => { + setImage(acceptedFiles[0]); + }, []); + + const { getRootProps, getInputProps, isDragActive, fileRejections } = useDropzone({ + onDrop, + accept: ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, + maxSize: MAX_FILE_SIZE, + }); + + const handleSubmit = async () => { + if (!image) return; + setIsImageUploading(true); + + const uploadCallback = (url: string) => { + onChange(url); + setIsImageUploading(false); + setImage(null); + setIsOpen(false); + }; + + if (isProfileCover) { + await fileService + .uploadUserAsset( + { + entity_identifier: "", + entity_type: EFileAssetType.USER_COVER, + }, + image + ) + .then((res) => uploadCallback(res.asset_url)) + .catch((error) => { + console.error("Error uploading user cover image:", error); + setIsImageUploading(false); + setToast({ + message: error?.error ?? "The image could not be uploaded", + type: TOAST_TYPE.ERROR, + title: "Image not uploaded", + }); + }); + } else { + if (!workspaceSlug) return; + await fileService + .uploadWorkspaceAsset( + workspaceSlug.toString(), + { + entity_identifier: projectId?.toString() ?? "", + entity_type: EFileAssetType.PROJECT_COVER, + }, + image + ) + .then((res) => uploadCallback(res.asset_url)) + .catch((error) => { + console.error("Error uploading project cover image:", error); + setIsImageUploading(false); + setToast({ + message: error?.error ?? "The image could not be uploaded", + type: TOAST_TYPE.ERROR, + title: "Image not uploaded", + }); + }); + } + }; + + const handleClose = () => { + if (isOpen) setIsOpen(false); + }; + + const toggleDropdown = () => { + setIsOpen((prevIsOpen) => !prevIsOpen); + }; + + const handleKeyDown = useDropdownKeyDown(toggleDropdown, handleClose); + + const handleOnClick = (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + toggleDropdown(); + }; + + useOutsideClickDetector(ref, handleClose); + + return ( + + + {label} + + + {isOpen && ( + +
    + + + {tabOptions.map((tab) => { + if (!unsplashImages && unsplashError && tab.key === "unsplash") return null; + if (projectCoverImages && projectCoverImages.length === 0 && tab.key === "images") return null; + + return ( + + `rounded px-4 py-1 text-center text-sm outline-none transition-colors ${ + selected ? "bg-custom-primary text-white" : "text-custom-text-100" + }` + } + > + {tab.title} + + ); + })} + + + {(unsplashImages || !unsplashError) && ( + +
    + ( + { + if (e.key === "Enter") { + e.preventDefault(); + setSearchParams(formData.search); + } + }} + value={value} + onChange={(e) => setFormData({ ...formData, search: e.target.value })} + ref={ref} + placeholder="Search for images" + className="w-full text-sm" + /> + )} + /> + +
    + {unsplashImages ? ( + unsplashImages.length > 0 ? ( +
    + {unsplashImages.map((image) => ( +
    { + setIsOpen(false); + onChange(image.urls.regular); + }} + > + {image.alt_description} +
    + ))} +
    + ) : ( +

    No images found.

    + ) + ) : ( + + + + + + + + + + + )} +
    + )} + {(!projectCoverImages || projectCoverImages.length !== 0) && ( + + {projectCoverImages ? ( + projectCoverImages.length > 0 ? ( +
    + {projectCoverImages.map((image, index) => ( +
    { + setIsOpen(false); + onChange(image); + }} + > + {`Default +
    + ))} +
    + ) : ( +

    No images found.

    + ) + ) : ( + + + + + + + + + + + )} +
    + )} + +
    +
    +
    + + {image !== null || (value && value !== "") ? ( + <> + image + + ) : ( +
    + + {isDragActive ? "Drop image here to upload" : "Drag & drop image here"} + +
    + )} + + +
    +
    + {fileRejections.length > 0 && ( +

    + {fileRejections[0].errors[0].code === "file-too-large" + ? "The image size cannot exceed 5 MB." + : "Please upload a file in a valid format."} +

    + )} + +

    File formats supported- .jpeg, .jpg, .png, .webp

    + +
    + + +
    +
    +
    +
    +
    +
    +
    + )} +
    + ); +}); diff --git a/dev-wiki/core/components/core/index.ts b/dev-wiki/core/components/core/index.ts new file mode 100644 index 0000000000..da7b4821d8 --- /dev/null +++ b/dev-wiki/core/components/core/index.ts @@ -0,0 +1,7 @@ +export * from "./filters"; +export * from "./sidebar"; +export * from "./theme"; +export * from "./image-picker-popover"; +export * from "./page-title"; +export * from "./app-header"; +export * from "./content-wrapper"; diff --git a/dev-wiki/core/components/core/list/index.ts b/dev-wiki/core/components/core/list/index.ts new file mode 100644 index 0000000000..d5489c45ea --- /dev/null +++ b/dev-wiki/core/components/core/list/index.ts @@ -0,0 +1,2 @@ +export * from "./list-item"; +export * from "./list-root"; diff --git a/dev-wiki/core/components/core/list/list-item.tsx b/dev-wiki/core/components/core/list/list-item.tsx new file mode 100644 index 0000000000..9a29f61c0e --- /dev/null +++ b/dev-wiki/core/components/core/list/list-item.tsx @@ -0,0 +1,110 @@ +"use client"; +import React, { FC } from "react"; +// ui +import { ControlLink, Row, Tooltip } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { useAppRouter } from "@/hooks/use-app-router"; + +interface IListItemProps { + id?: string; + title: string; + itemLink: string; + onItemClick?: (e: React.MouseEvent) => void; + prependTitleElement?: JSX.Element; + appendTitleElement?: JSX.Element; + actionableItems?: JSX.Element; + isMobile?: boolean; + parentRef: React.RefObject; + disableLink?: boolean; + className?: string; + itemClassName?: string; + actionItemContainerClassName?: string; + isSidebarOpen?: boolean; + quickActionElement?: JSX.Element; + preventDefaultNProgress?: boolean; + leftElementClassName?: string; + rightElementClassName?: string; +} + +export const ListItem: FC = (props) => { + const { + id, + title, + prependTitleElement, + appendTitleElement, + actionableItems, + itemLink, + onItemClick, + isMobile = false, + parentRef, + disableLink = false, + className = "", + actionItemContainerClassName = "", + isSidebarOpen = false, + quickActionElement, + itemClassName = "", + preventDefaultNProgress = false, + leftElementClassName = "", + rightElementClassName = "", + } = props; + + // router + const router = useAppRouter(); + + // handlers + const handleControlLinkClick = (e: React.MouseEvent) => { + if (onItemClick) onItemClick(e); + else router.push(itemLink); + }; + + return ( +
    + +
    + +
    + {prependTitleElement && {prependTitleElement}} + + {title} + +
    + {appendTitleElement && ( + {appendTitleElement} + )} +
    + {quickActionElement && quickActionElement} +
    + {actionableItems && ( +
    + {actionableItems} +
    + )} +
    +
    + ); +}; diff --git a/dev-wiki/core/components/core/list/list-root.tsx b/dev-wiki/core/components/core/list/list-root.tsx new file mode 100644 index 0000000000..bd62efb2ef --- /dev/null +++ b/dev-wiki/core/components/core/list/list-root.tsx @@ -0,0 +1,18 @@ +import React, { FC } from "react"; +import { Row, ERowVariant } from "@plane/ui"; + +interface IListContainer { + children: React.ReactNode; +} + +export const ListLayout: FC = (props) => { + const { children } = props; + return ( + + {children} + + ); +}; diff --git a/dev-wiki/core/components/core/page-title.tsx b/dev-wiki/core/components/core/page-title.tsx new file mode 100644 index 0000000000..554a2e0f1f --- /dev/null +++ b/dev-wiki/core/components/core/page-title.tsx @@ -0,0 +1,14 @@ +import { useHead } from "@plane/ui"; + +type PageHeadTitleProps = { + title?: string; + description?: string; +}; + +export const PageHead: React.FC = (props) => { + const { title } = props; + + useHead({ title }); + + return null; +}; diff --git a/dev-wiki/core/components/core/render-if-visible-HOC.tsx b/dev-wiki/core/components/core/render-if-visible-HOC.tsx new file mode 100644 index 0000000000..ea3fc9c2f8 --- /dev/null +++ b/dev-wiki/core/components/core/render-if-visible-HOC.tsx @@ -0,0 +1,86 @@ +import React, { useState, useRef, useEffect, ReactNode, MutableRefObject } from "react"; +import { cn } from "@/helpers/common.helper"; + +type Props = { + defaultHeight?: string; + verticalOffset?: number; + horizontalOffset?: number; + root?: MutableRefObject; + children: ReactNode; + as?: keyof JSX.IntrinsicElements; + classNames?: string; + placeholderChildren?: ReactNode; + defaultValue?: boolean; + shouldRecordHeights?: boolean; + useIdletime?: boolean; + forceRender?: boolean; +}; + +const RenderIfVisible: React.FC = (props) => { + const { + defaultHeight = "300px", + root, + verticalOffset = 50, + horizontalOffset = 0, + as = "div", + children, + classNames = "", + shouldRecordHeights = true, + //placeholder children + placeholderChildren = null, //placeholder children + defaultValue = false, + useIdletime = false, + forceRender = false, + } = props; + const [shouldVisible, setShouldVisible] = useState(defaultValue); + const placeholderHeight = useRef(defaultHeight); + const intersectionRef = useRef(null); + + const isVisible = shouldVisible || forceRender; + + // Set visibility with intersection observer + useEffect(() => { + if (intersectionRef.current) { + const observer = new IntersectionObserver( + (entries) => { + //DO no remove comments for future + if (typeof window !== undefined && window.requestIdleCallback && useIdletime) { + window.requestIdleCallback(() => setShouldVisible(entries[entries.length - 1].isIntersecting), { + timeout: 300, + }); + } else { + setShouldVisible(entries[entries.length - 1].isIntersecting); + } + }, + { + root: root?.current, + rootMargin: `${verticalOffset}% ${horizontalOffset}% ${verticalOffset}% ${horizontalOffset}%`, + } + ); + observer.observe(intersectionRef.current); + return () => { + if (intersectionRef.current) { + // eslint-disable-next-line react-hooks/exhaustive-deps + observer.unobserve(intersectionRef.current); + } + }; + } + }, [intersectionRef, children, root, verticalOffset, horizontalOffset]); + + //Set height after render + useEffect(() => { + if (intersectionRef.current && isVisible && shouldRecordHeights) { + window.requestIdleCallback(() => { + if (intersectionRef.current) placeholderHeight.current = `${intersectionRef.current.offsetHeight}px`; + }); + } + }, [isVisible, intersectionRef, shouldRecordHeights]); + + const child = isVisible ? <>{children} : placeholderChildren; + const style = isVisible || !shouldRecordHeights ? {} : { height: placeholderHeight.current, width: "100%" }; + const className = isVisible || placeholderChildren ? classNames : cn(classNames, "bg-custom-background-80"); + + return React.createElement(as, { ref: intersectionRef, style, className }, child); +}; + +export default RenderIfVisible; diff --git a/dev-wiki/core/components/core/sidebar/index.ts b/dev-wiki/core/components/core/sidebar/index.ts new file mode 100644 index 0000000000..ad5c6f6835 --- /dev/null +++ b/dev-wiki/core/components/core/sidebar/index.ts @@ -0,0 +1,2 @@ +export * from "./single-progress-stats"; +export * from "./sidebar-menu-hamburger-toggle"; diff --git a/dev-wiki/core/components/core/sidebar/progress-chart.tsx b/dev-wiki/core/components/core/sidebar/progress-chart.tsx new file mode 100644 index 0000000000..e6317eeb9f --- /dev/null +++ b/dev-wiki/core/components/core/sidebar/progress-chart.tsx @@ -0,0 +1,69 @@ +import React from "react"; +import { AreaChart } from "@plane/propel/charts/area-chart"; +import { TChartData, TModuleCompletionChartDistribution } from "@plane/types"; +import { renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; + +type Props = { + distribution: TModuleCompletionChartDistribution; + totalIssues: number; + className?: string; + plotTitle?: string; +}; + +const ProgressChart: React.FC = ({ distribution, totalIssues, className = "", plotTitle = "work items" }) => { + const chartData: TChartData[] = Object.keys(distribution ?? []).map((key, index) => ({ + name: renderFormattedDateWithoutYear(key), + current: distribution[key] ?? 0, + ideal: totalIssues * (1 - index / (Object.keys(distribution ?? []).length - 1)), + })); + + return ( +
    + +
    + ); +}; + +export default ProgressChart; diff --git a/dev-wiki/core/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx b/dev-wiki/core/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx new file mode 100644 index 0000000000..7dc3988129 --- /dev/null +++ b/dev-wiki/core/components/core/sidebar/sidebar-menu-hamburger-toggle.tsx @@ -0,0 +1,20 @@ +"use client"; + +import { observer } from "mobx-react"; +import { Menu } from "lucide-react"; +import { useAppTheme } from "@/hooks/store"; + +export const SidebarHamburgerToggle = observer(() => { + // store hooks + const { toggleSidebar } = useAppTheme(); + + return ( + + ); +}); diff --git a/dev-wiki/core/components/core/sidebar/single-progress-stats.tsx b/dev-wiki/core/components/core/sidebar/single-progress-stats.tsx new file mode 100644 index 0000000000..047d8bc04d --- /dev/null +++ b/dev-wiki/core/components/core/sidebar/single-progress-stats.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +type TSingleProgressStatsProps = { + title: any; + completed: number; + total: number; + onClick?: () => void; + selected?: boolean; +}; + +export const SingleProgressStats: React.FC = ({ + title, + completed, + total, + onClick, + selected = false, +}) => ( +
    +
    {title}
    +
    +
    + + {isNaN(Math.round((completed / total) * 100)) ? "0" : Math.round((completed / total) * 100)}% + +
    + of {total} +
    +
    +); diff --git a/dev-wiki/core/components/core/theme/color-picker-input.tsx b/dev-wiki/core/components/core/theme/color-picker-input.tsx new file mode 100644 index 0000000000..dbaa13aacb --- /dev/null +++ b/dev-wiki/core/components/core/theme/color-picker-input.tsx @@ -0,0 +1,129 @@ +"use client"; +import { FC, Fragment } from "react"; +// react-form +import { ColorResult, SketchPicker } from "react-color"; +import { + Control, + Controller, + FieldError, + FieldErrorsImpl, + Merge, + UseFormRegister, + UseFormSetValue, + UseFormWatch, +} from "react-hook-form"; +// react-color +// component +import { Palette } from "lucide-react"; +import { Popover, Transition } from "@headlessui/react"; +import { IUserTheme } from "@plane/types"; +import { Input } from "@plane/ui"; +// icons +// types + +type Props = { + name: keyof IUserTheme; + position?: "left" | "right"; + watch: UseFormWatch; + setValue: UseFormSetValue; + control: Control; + error: FieldError | Merge> | undefined; + register: UseFormRegister; +}; + +export const ColorPickerInput: FC = (props) => { + const { name, position = "left", watch, setValue, error, control } = props; + + const handleColorChange = (newColor: ColorResult) => { + const { hex } = newColor; + setValue(name, hex); + }; + + const getColorText = (colorName: keyof IUserTheme) => { + switch (colorName) { + case "background": + return "Background"; + case "text": + return "Text"; + case "primary": + return "Primary(Theme)"; + case "sidebarBackground": + return "Sidebar Background"; + case "sidebarText": + return "Sidebar Text"; + default: + return "Color"; + } + }; + + return ( +
    + ( + + )} + /> +
    + + {({ open }) => ( + <> + + {watch(name) && watch(name) !== "" ? ( + + ) : ( + + )} + + + + + + + + + )} + +
    +
    + ); +}; diff --git a/dev-wiki/core/components/core/theme/custom-theme-selector.tsx b/dev-wiki/core/components/core/theme/custom-theme-selector.tsx new file mode 100644 index 0000000000..d66fbe82f8 --- /dev/null +++ b/dev-wiki/core/components/core/theme/custom-theme-selector.tsx @@ -0,0 +1,243 @@ +"use client"; + +import { useMemo } from "react"; +import { observer } from "mobx-react"; +import { Controller, useForm } from "react-hook-form"; +// types +import { useTranslation } from "@plane/i18n"; +import { IUserTheme } from "@plane/types"; +// ui +import { Button, InputColorPicker, setPromiseToast } from "@plane/ui"; +// hooks +import { useUserProfile } from "@/hooks/store"; + +type TCustomThemeSelector = { + applyThemeChange: (theme: Partial) => void; +}; + +export const CustomThemeSelector: React.FC = observer((props) => { + const { applyThemeChange } = props; + // hooks + const { data: userProfile, updateUserTheme } = useUserProfile(); + const { t } = useTranslation(); + const { + control, + formState: { errors, isSubmitting }, + handleSubmit, + watch, + } = useForm({ + defaultValues: { + background: userProfile?.theme?.background !== "" ? userProfile?.theme?.background : "#0d101b", + text: userProfile?.theme?.text !== "" ? userProfile?.theme?.text : "#c5c5c5", + primary: userProfile?.theme?.primary !== "" ? userProfile?.theme?.primary : "#3f76ff", + sidebarBackground: + userProfile?.theme?.sidebarBackground !== "" ? userProfile?.theme?.sidebarBackground : "#0d101b", + sidebarText: userProfile?.theme?.sidebarText !== "" ? userProfile?.theme?.sidebarText : "#c5c5c5", + darkPalette: userProfile?.theme?.darkPalette || false, + palette: userProfile?.theme?.palette !== "" ? userProfile?.theme?.palette : "", + }, + }); + + const inputRules = useMemo( + () => ({ + minLength: { + value: 7, + message: t("enter_a_valid_hex_code_of_6_characters"), + }, + maxLength: { + value: 7, + message: t("enter_a_valid_hex_code_of_6_characters"), + }, + pattern: { + value: /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, + message: t("enter_a_valid_hex_code_of_6_characters"), + }, + }), + [t] // Empty dependency array since these rules never change + ); + + const handleUpdateTheme = async (formData: Partial) => { + const payload: IUserTheme = { + background: formData.background, + text: formData.text, + primary: formData.primary, + sidebarBackground: formData.sidebarBackground, + sidebarText: formData.sidebarText, + darkPalette: false, + palette: `${formData.background},${formData.text},${formData.primary},${formData.sidebarBackground},${formData.sidebarText}`, + theme: "custom", + }; + applyThemeChange(payload); + + const updateCurrentUserThemePromise = updateUserTheme(payload); + setPromiseToast(updateCurrentUserThemePromise, { + loading: t("updating_theme"), + success: { + title: t("success"), + message: () => t("theme_updated_successfully"), + }, + error: { + title: t("error"), + message: () => t("failed_to_update_the_theme"), + }, + }); + + return; + }; + + const handleValueChange = (val: string | undefined, onChange: any) => { + let hex = val; + // prepend a hashtag if it doesn't exist + if (val && val[0] !== "#") hex = `#${val}`; + + onChange(hex); + }; + + return ( +
    +
    +

    {t("customize_your_theme")}

    +
    +
    +
    +

    {t("background_color")}

    +
    + ( + handleValueChange(val, onChange)} + placeholder="#0d101b" + className="w-full placeholder:text-custom-text-400/60" + style={{ + backgroundColor: watch("background"), + color: watch("text"), + }} + hasError={Boolean(errors?.background)} + /> + )} + /> + {errors.background &&

    {errors.background.message}

    } +
    +
    + +
    +

    {t("text_color")}

    +
    + ( + handleValueChange(val, onChange)} + placeholder="#c5c5c5" + className="w-full placeholder:text-custom-text-400/60" + style={{ + backgroundColor: watch("text"), + color: watch("background"), + }} + hasError={Boolean(errors?.text)} + /> + )} + /> + {errors.text &&

    {errors.text.message}

    } +
    +
    + +
    +

    {t("primary_color")}

    +
    + ( + handleValueChange(val, onChange)} + placeholder="#3f76ff" + className="w-full placeholder:text-custom-text-400/60" + style={{ + backgroundColor: watch("primary"), + color: watch("text"), + }} + hasError={Boolean(errors?.primary)} + /> + )} + /> + {errors.primary &&

    {errors.primary.message}

    } +
    +
    + +
    +

    {t("sidebar_background_color")}

    +
    + ( + handleValueChange(val, onChange)} + placeholder="#0d101b" + className="w-full placeholder:text-custom-text-400/60" + style={{ + backgroundColor: watch("sidebarBackground"), + color: watch("sidebarText"), + }} + hasError={Boolean(errors?.sidebarBackground)} + /> + )} + /> + {errors.sidebarBackground && ( +

    {errors.sidebarBackground.message}

    + )} +
    +
    + +
    +

    {t("sidebar_text_color")}

    +
    + ( + handleValueChange(val, onChange)} + placeholder="#c5c5c5" + className="w-full placeholder:text-custom-text-400/60" + style={{ + backgroundColor: watch("sidebarText"), + color: watch("sidebarBackground"), + }} + hasError={Boolean(errors?.sidebarText)} + /> + )} + /> + {errors.sidebarText &&

    {errors.sidebarText.message}

    } +
    +
    +
    +
    +
    +
    + +
    +
    + ); +}); diff --git a/dev-wiki/core/components/core/theme/index.ts b/dev-wiki/core/components/core/theme/index.ts new file mode 100644 index 0000000000..145b3862a1 --- /dev/null +++ b/dev-wiki/core/components/core/theme/index.ts @@ -0,0 +1,3 @@ +export * from "./color-picker-input"; +export * from "./custom-theme-selector"; +export * from "./theme-switch"; diff --git a/dev-wiki/core/components/core/theme/theme-switch.tsx b/dev-wiki/core/components/core/theme/theme-switch.tsx new file mode 100644 index 0000000000..c6677ac293 --- /dev/null +++ b/dev-wiki/core/components/core/theme/theme-switch.tsx @@ -0,0 +1,83 @@ +"use client"; + +import { FC } from "react"; +// plane imports +import { I_THEME_OPTION, THEME_OPTIONS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +// constants +import { CustomSelect } from "@plane/ui"; +// ui + +type Props = { + value: I_THEME_OPTION | null; + onChange: (value: I_THEME_OPTION) => void; +}; + +export const ThemeSwitch: FC = (props) => { + const { value, onChange } = props; + const { t } = useTranslation(); + return ( + +
    +
    +
    +
    + {t(value.key)} +
    + ) : ( + t("select_your_theme") + ) + } + onChange={onChange} + input + > + {THEME_OPTIONS.map((themeOption) => ( + +
    +
    +
    +
    +
    + {t(themeOption.key)} +
    + + ))} + + ); +}; diff --git a/dev-wiki/core/components/editor/embeds/index.ts b/dev-wiki/core/components/editor/embeds/index.ts new file mode 100644 index 0000000000..8146e94d92 --- /dev/null +++ b/dev-wiki/core/components/editor/embeds/index.ts @@ -0,0 +1 @@ +export * from "./mentions"; diff --git a/dev-wiki/core/components/editor/embeds/mentions/index.ts b/dev-wiki/core/components/editor/embeds/mentions/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/dev-wiki/core/components/editor/embeds/mentions/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/dev-wiki/core/components/editor/embeds/mentions/root.tsx b/dev-wiki/core/components/editor/embeds/mentions/root.tsx new file mode 100644 index 0000000000..d1bc8e9b54 --- /dev/null +++ b/dev-wiki/core/components/editor/embeds/mentions/root.tsx @@ -0,0 +1,17 @@ +// plane editor +import { TMentionComponentProps } from "@plane/editor"; +// plane web components +// import { EditorAdditionalMentionsRoot } from "@/ce/components/editor"; +// local components +import { EditorUserMention } from "./user"; + +export const EditorMentionsRoot: React.FC = (props) => { + const { entity_identifier, entity_name } = props; + + switch (entity_name) { + case "user_mention": + return ; + // default: + // return ; + } +}; diff --git a/dev-wiki/core/components/editor/embeds/mentions/user.tsx b/dev-wiki/core/components/editor/embeds/mentions/user.tsx new file mode 100644 index 0000000000..f49d1792ca --- /dev/null +++ b/dev-wiki/core/components/editor/embeds/mentions/user.tsx @@ -0,0 +1,99 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { usePopper } from "react-popper"; +// plane imports +import { ROLE } from "@plane/constants"; +// plane ui +import { Avatar } from "@plane/ui"; +// constants +// helpers +import { cn } from "@/helpers/common.helper"; +import { getFileURL } from "@/helpers/file.helper"; +// hooks +import { useMember, useUser } from "@/hooks/store"; + +type Props = { + id: string; +}; + +export const EditorUserMention: React.FC = observer((props) => { + const { id } = props; + // router + const { projectId } = useParams(); + // states + const [popperElement, setPopperElement] = useState(null); + const [referenceElement, setReferenceElement] = useState(null); + // params + const { workspaceSlug } = useParams(); + // store hooks + const { data: currentUser } = useUser(); + const { + getUserDetails, + project: { getProjectMemberDetails }, + } = useMember(); + // popper-js refs + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + // derived values + const userDetails = getUserDetails(id); + const roleDetails = projectId ? getProjectMemberDetails(id, projectId.toString())?.role : null; + const profileLink = `/${workspaceSlug}/profile/${id}`; + + if (!userDetails) { + return ( +
    + @deactivated user +
    + ); + } + + return ( +
    + + @{userDetails?.display_name} + +
    +
    +
    + +
    +
    + + {userDetails?.first_name} {userDetails?.last_name} + + {roleDetails &&

    {ROLE[roleDetails as keyof typeof ROLE]}

    } +
    +
    +
    +
    + ); +}); diff --git a/dev-wiki/core/components/editor/index.ts b/dev-wiki/core/components/editor/index.ts new file mode 100644 index 0000000000..cf8352ae4a --- /dev/null +++ b/dev-wiki/core/components/editor/index.ts @@ -0,0 +1 @@ +export * from "./embeds"; diff --git a/dev-wiki/core/components/editor/index.tsx b/dev-wiki/core/components/editor/index.tsx new file mode 100644 index 0000000000..a891fa82b8 --- /dev/null +++ b/dev-wiki/core/components/editor/index.tsx @@ -0,0 +1,2 @@ +export const RichTextReadOnlyEditor = () => null; +export const EditorMentionsRoot = () => null; export const ProductUpdatesHeader = () => null; diff --git a/dev-wiki/core/components/empty-state/index.tsx b/dev-wiki/core/components/empty-state/index.tsx new file mode 100644 index 0000000000..4ae97e839c --- /dev/null +++ b/dev-wiki/core/components/empty-state/index.tsx @@ -0,0 +1,107 @@ +"use client"; + +import React from "react"; +import { observer } from "mobx-react"; +import Image from "next/image"; +// ui +import { Button } from "@plane/ui/src/button"; +// utils +import { cn } from "@plane/utils"; + +type EmptyStateSize = "sm" | "md" | "lg"; + +type ButtonConfig = { + text: string; + prependIcon?: React.ReactNode; + appendIcon?: React.ReactNode; + onClick?: () => void; + disabled?: boolean; +}; + +type Props = { + title: string; + description?: string; + assetPath?: string; + size?: EmptyStateSize; + primaryButton?: ButtonConfig; + secondaryButton?: ButtonConfig; + customPrimaryButton?: React.ReactNode; + customSecondaryButton?: React.ReactNode; + className?: string; +}; + +const sizeClasses = { + sm: "md:min-w-[24rem] max-w-[45rem]", + md: "md:min-w-[28rem] max-w-[50rem]", + lg: "md:min-w-[30rem] max-w-[60rem]", +} as const; + +const CustomButton = ({ + config, + variant, + size, +}: { + config: ButtonConfig; + variant: "primary" | "neutral-primary"; + size: EmptyStateSize; +}) => ( + +); + +export const DetailedEmptyState: React.FC = observer((props) => { + const { + title, + description, + size = "lg", + primaryButton, + secondaryButton, + customPrimaryButton, + customSecondaryButton, + assetPath, + className, + } = props; + + const hasButtons = primaryButton || secondaryButton || customPrimaryButton || customSecondaryButton; + + return ( +
    +
    +
    +

    {title}

    + {description &&

    {description}

    } +
    + + {assetPath && ( + {title} + )} + + {hasButtons && ( +
    + {/* primary button */} + {customPrimaryButton ?? + (primaryButton?.text && )} + {/* secondary button */} + {customSecondaryButton ?? + (secondaryButton?.text && ( + + ))} +
    + )} +
    +
    + ); +}); diff --git a/dev-wiki/core/components/instance/index.ts b/dev-wiki/core/components/instance/index.ts new file mode 100644 index 0000000000..43e76eba72 --- /dev/null +++ b/dev-wiki/core/components/instance/index.ts @@ -0,0 +1,2 @@ +export * from "./not-ready-view"; +export * from "./maintenance-view"; diff --git a/dev-wiki/core/components/instance/maintenance-view.tsx b/dev-wiki/core/components/instance/maintenance-view.tsx new file mode 100644 index 0000000000..8dd4c34e78 --- /dev/null +++ b/dev-wiki/core/components/instance/maintenance-view.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { FC } from "react"; +import Image from "next/image"; +// ui +import { Button } from "@plane/ui"; +// layouts +import DefaultLayout from "@/layouts/default-layout"; +// components +import { MaintenanceMessage } from "@/plane-web/components/instance"; +// images +import maintenanceModeImage from "@/public/maintenance-mode.webp"; + +export const MaintenanceView: FC = () => ( + +
    +
    + ProjectSettingImg +
    +
    + + +
    +
    +
    +); diff --git a/dev-wiki/core/components/instance/not-ready-view.tsx b/dev-wiki/core/components/instance/not-ready-view.tsx new file mode 100644 index 0000000000..8d317634a8 --- /dev/null +++ b/dev-wiki/core/components/instance/not-ready-view.tsx @@ -0,0 +1,62 @@ +"use client"; + +import { FC } from "react"; +import Image from "next/image"; +import Link from "next/link"; +import { useTheme } from "next-themes"; +import { Button } from "@plane/ui"; +// helpers +import { GOD_MODE_URL } from "@/helpers/common.helper"; +// images +// assets +import PlaneBackgroundPatternDark from "@/public/auth/background-pattern-dark.svg"; +import PlaneBackgroundPattern from "@/public/auth/background-pattern.svg"; +import BlackHorizontalLogo from "@/public/plane-logos/black-horizontal-with-blue-logo.png"; +import WhiteHorizontalLogo from "@/public/plane-logos/white-horizontal-with-blue-logo.png"; +import PlaneTakeOffImage from "@/public/plane-takeoff.png"; + +export const InstanceNotReady: FC = () => { + const { resolvedTheme } = useTheme(); + const patternBackground = resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern; + + const logo = resolvedTheme === "light" ? BlackHorizontalLogo : WhiteHorizontalLogo; + + return ( +
    +
    +
    +
    + + Plane logo + +
    +
    + +
    + Plane background pattern +
    + +
    +
    +
    +
    +

    Welcome aboard Plane!

    + Plane Logo +

    + Get started by setting up your instance and workspace +

    +
    + +
    +
    +
    +
    +
    + ); +}; diff --git a/dev-wiki/core/components/issues/issue-layouts/properties/all-properties.tsx b/dev-wiki/core/components/issues/issue-layouts/properties/all-properties.tsx new file mode 100644 index 0000000000..c6aeb549ea --- /dev/null +++ b/dev-wiki/core/components/issues/issue-layouts/properties/all-properties.tsx @@ -0,0 +1 @@ +export const IssueProperties = () => null; diff --git a/dev-wiki/core/components/issues/issue-layouts/utils.ts b/dev-wiki/core/components/issues/issue-layouts/utils.ts new file mode 100644 index 0000000000..1dadc78345 --- /dev/null +++ b/dev-wiki/core/components/issues/issue-layouts/utils.ts @@ -0,0 +1,12 @@ +// Minimal utils for dev-wiki - only what's needed for sidebar functionality +export const HIGHLIGHT_CLASS = "bg-yellow-50 border-yellow-200"; + +export const highlightIssueOnDrop = (elementId: string, timeout: number = 3000) => { + const element = document.getElementById(elementId); + if (element) { + element.classList.add(HIGHLIGHT_CLASS); + setTimeout(() => { + element.classList.remove(HIGHLIGHT_CLASS); + }, timeout); + } +}; diff --git a/dev-wiki/core/components/pages/dropdowns/actions.tsx b/dev-wiki/core/components/pages/dropdowns/actions.tsx new file mode 100644 index 0000000000..ffe8208d0a --- /dev/null +++ b/dev-wiki/core/components/pages/dropdowns/actions.tsx @@ -0,0 +1,194 @@ +"use client"; +import { useMemo, useState } from "react"; +import { observer } from "mobx-react"; +import { Copy, ExternalLink, Globe2, Link, Lock, Trash2 } from "lucide-react"; +// constants +import { EPageAccess } from "@plane/constants"; +// plane editor +import { EditorRefApi } from "@plane/editor"; +// plane ui +import { ContextMenu, CustomMenu, TContextMenuItem } from "@plane/ui"; +// components +import { DeletePageModal } from "@/components/pages"; +// helpers +import { cn } from "@/helpers/common.helper"; +// hooks +import { usePageOperations } from "@/hooks/use-page-operations"; +// plane web hooks +import { EPageStoreType } from "@/plane-web/hooks/store/use-page-store"; +// Import the custom menu hook +import { usePageActionsMenu } from "@/plane-web/hooks/use-page-actions-menu"; +// store types +import { TPageInstance } from "@/store/pages/base-page"; + +export type TPageActions = + | "full-screen" + | "sticky-toolbar" + | "copy-markdown" + | "toggle-lock" + | "toggle-access" + | "open-in-new-tab" + | "copy-link" + | "make-a-copy" + | "archive-restore" + | "delete" + | "version-history" + | "export" + | "move"; + +type Props = { + editorRef?: EditorRefApi | null; + extraOptions?: (TContextMenuItem & { key: TPageActions })[]; + optionsOrder: TPageActions[]; + page: TPageInstance; + parentRef?: React.RefObject; + storeType: EPageStoreType; + realtimeEvents?: boolean; +}; + +export const PageActions: React.FC = observer((props) => { + const { editorRef, extraOptions, optionsOrder, page, parentRef, storeType, realtimeEvents = true } = props; + + // states for common modals + const [deletePageModal, setDeletePageModal] = useState(false); + + // page operations + const { pageOperations } = usePageOperations({ + editorRef, + page, + }); + + // Get custom menu items and modals from the environment-specific implementation + const { customMenuItems, ModalsComponent } = usePageActionsMenu({ + page, + storeType, + pageOperations, + }); + + // derived values + const { access, archived_at, canCurrentUserChangeAccess, canCurrentUserDeletePage, canCurrentUserDuplicatePage } = + page; + + // Base menu items that are common across all implementations + const baseMenuItems: (TContextMenuItem & { key: TPageActions })[] = useMemo( + () => [ + { + key: "toggle-access", + action: pageOperations.toggleAccess, + title: access === EPageAccess.PUBLIC ? "Make private" : "Make public", + icon: access === EPageAccess.PUBLIC ? Lock : Globe2, + shouldRender: canCurrentUserChangeAccess && !archived_at, + }, + { + key: "open-in-new-tab", + action: pageOperations.openInNewTab, + title: "Open in new tab", + icon: ExternalLink, + shouldRender: true, + }, + { + key: "copy-link", + action: pageOperations.copyLink, + title: "Copy link", + icon: Link, + shouldRender: true, + }, + { + key: "make-a-copy", + action: () => pageOperations.duplicate(realtimeEvents), + title: "Make a copy", + icon: Copy, + shouldRender: canCurrentUserDuplicatePage, + }, + { + key: "delete", + action: () => setDeletePageModal(true), + title: "Delete", + icon: Trash2, + shouldRender: canCurrentUserDeletePage && !!archived_at, + }, + ], + [ + access, + archived_at, + canCurrentUserChangeAccess, + canCurrentUserDeletePage, + canCurrentUserDuplicatePage, + pageOperations, + realtimeEvents, + ] + ); + + // Merge base menu items with custom menu items + const MENU_ITEMS: (TContextMenuItem & { key: TPageActions })[] = useMemo(() => { + // Start with base menu items + const menuItems = [...baseMenuItems]; + + // Add custom menu items + customMenuItems.forEach((customItem) => { + // Find if there's already an item with the same key + const existingIndex = menuItems.findIndex((item) => item.key === customItem.key); + + if (existingIndex >= 0) { + // Replace the existing item + menuItems[existingIndex] = customItem; + } else { + // Add as a new item + menuItems.push(customItem); + } + }); + + // Add extra options if provided + if (extraOptions) { + menuItems.push(...extraOptions); + } + + return menuItems; + }, [baseMenuItems, customMenuItems, extraOptions]); + + // arrange options + const arrangedOptions = useMemo( + () => + optionsOrder + .map((key) => MENU_ITEMS.find((item) => item.key === key)) + .filter((item) => !!item) as (TContextMenuItem & { key: TPageActions })[], + [optionsOrder, MENU_ITEMS] + ); + + return ( + <> + setDeletePageModal(false)} + page={page} + storeType={storeType} + /> + + {parentRef && } + + {arrangedOptions.map((item) => { + if (item.shouldRender === false) return null; + return ( + { + e.preventDefault(); + e.stopPropagation(); + item.action?.(); + }} + className={cn("flex items-center gap-2", item.className)} + disabled={item.disabled} + > + {item.customContent ?? ( + <> + {item.icon && } + {item.title} + + )} + + ); + })} + + + ); +}); diff --git a/dev-wiki/core/components/pages/dropdowns/index.ts b/dev-wiki/core/components/pages/dropdowns/index.ts new file mode 100644 index 0000000000..8ff5a89aff --- /dev/null +++ b/dev-wiki/core/components/pages/dropdowns/index.ts @@ -0,0 +1 @@ +export * from "./actions"; diff --git a/dev-wiki/core/components/pages/editor/breadcrumb-page-item.tsx b/dev-wiki/core/components/pages/editor/breadcrumb-page-item.tsx new file mode 100644 index 0000000000..8ab5d0a4ee --- /dev/null +++ b/dev-wiki/core/components/pages/editor/breadcrumb-page-item.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useState } from "react"; +import { observer } from "mobx-react"; +import { FileText } from "lucide-react"; +// types +// ui +import { EmojiIconPicker, EmojiIconPickerTypes, Tooltip } from "@plane/ui"; +// components +import { BreadcrumbLink, Logo } from "@/components/common"; +// helpers +import { getPageName } from "@/helpers/page.helper"; +// hooks +import { usePlatformOS } from "@/hooks/use-platform-os"; +// plane web hooks +import { usePage } from "@/plane-web/hooks/store/use-page"; +import { EPageStoreType } from "@/plane-web/hooks/store/use-page-store"; + +export interface IPageBreadcrumbProps { + pageId: string; + storeType: EPageStoreType; + href?: string; + showLogo?: boolean; + isEditable?: boolean; +} + +export const PageBreadcrumbItem: React.FC = observer( + ({ pageId, storeType, href, showLogo = true, isEditable }) => { + // states + const [isOpen, setIsOpen] = useState(false); + + // hooks + const { isMobile } = usePlatformOS(); + const page = usePage({ + pageId: pageId, + storeType: storeType, + }); + + const { name, logo_props, updatePageLogo, isContentEditable } = page ?? {}; + + // Show loading state when the page data isn't available yet + if (!page) { + const loadingItemClasses = href ? "" : "flex items-center space-x-2"; + + if (href) { + return ( +
    + {showLogo &&
    } +
    +
    + ); + } + + return ( +
  • +
    +
    + {showLogo &&
    } +
    +
    +
    +
  • + ); + } + + if (href) { + return ( + + {logo_props?.in_use ? ( + + ) : ( + + )} + + ) + } + /> + ); + } + + return ( +
  • +
    +
    + {showLogo && ( +
    + setIsOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" + label={ + <> + {logo_props?.in_use ? ( + + ) : ( + + )} + + } + onChange={(val) => updatePageLogo?.(val)} + defaultIconColor={ + logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined + } + defaultOpen={ + logo_props?.in_use && logo_props?.in_use === "emoji" + ? EmojiIconPickerTypes.EMOJI + : EmojiIconPickerTypes.ICON + } + disabled={!isEditable && !isContentEditable} + /> +
    + )} + +
    + {getPageName(name)} +
    +
    +
    +
    +
  • + ); + } +); diff --git a/dev-wiki/core/components/pages/editor/editor-body.tsx b/dev-wiki/core/components/pages/editor/editor-body.tsx new file mode 100644 index 0000000000..7fd736f1bf --- /dev/null +++ b/dev-wiki/core/components/pages/editor/editor-body.tsx @@ -0,0 +1,331 @@ +import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { observer } from "mobx-react"; +// document-editor +import { + CollaborativeDocumentEditorWithRef, + EditorRefApi, + EditorTitleRefApi, + TAIMenuProps, + TDisplayConfig, + TFileHandler, + TRealtimeConfig, + TServerHandler, +} from "@plane/editor"; +// plane types +import { TSearchEntityRequestPayload, TSearchResponse, TWebhookConnectionQueryParams } from "@plane/types"; +// plane ui +import { ERowVariant, Row } from "@plane/ui"; +import { cn, HSL, hslToHex, isCommentEmpty } from "@plane/utils"; +// components +import { EditorMentionsRoot } from "@/components/editor"; +import { PageContentBrowser, PageContentLoader } from "@/components/pages"; +// helpers +import { LIVE_BASE_PATH, LIVE_BASE_URL } from "@/helpers/common.helper"; +// hooks +import { useEditorMention } from "@/hooks/editor"; +import { useMember, useUser, useUserProfile, useWorkspace } from "@/hooks/store"; +import { usePageFilters } from "@/hooks/use-page-filters"; +import { type TCustomEventHandlers, useRealtimePageEvents } from "@/hooks/use-realtime-page-events"; +// plane web components +import { EditorAIMenu } from "@/plane-web/components/pages"; +import { MultipleDeletePagesModal } from "@/plane-web/components/pages/modals/multiple-page-delete-modal"; +// plane web store +import { EPageStoreType } from "@/plane-web/hooks/store/use-page-store"; +// plane web hooks +import { useEditorEmbeds } from "@/plane-web/hooks/use-editor-embed"; +import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +import { PageEditorHeaderRoot } from "./header"; + +// Add a CSS keyframe animation +export type TEditorBodyConfig = { + fileHandler: TFileHandler; +}; + +// Define the structure of action-specific data +export type TEditorBodyHandlers = { + fetchEntity: (payload: TSearchEntityRequestPayload) => Promise; + getRedirectionLink: (pageId?: string) => string; +}; + +type Props = { + config: TEditorBodyConfig; + editorReady: boolean; + editorForwardRef: React.RefObject; + handleConnectionStatus: Dispatch>; + handleEditorReady: (status: boolean) => void; + handlers: TEditorBodyHandlers; + page: TPageInstance; + webhookConnectionParams: TWebhookConnectionQueryParams; + projectId?: string; + workspaceSlug: string; + storeType: EPageStoreType; + customRealtimeEventHandlers?: TCustomEventHandlers; +}; + +export const generateRandomColor = (input: string): HSL => { + // If input is falsy, generate a random seed string. + // The random seed is created by converting a random number to base-36 and taking a substring. + const seed = input || Math.random().toString(36).substring(2, 8); + + const uniqueId = seed.length.toString() + seed; // Unique identifier based on string length + const combinedString = uniqueId + seed; + + // Create a hash value from the combined string. + const hash = Array.from(combinedString).reduce((acc, char) => { + const charCode = char.charCodeAt(0); + return (acc << 5) - acc + charCode; + }, 0); + + // Derive the HSL values from the hash. + const hue = Math.abs(hash % 360); + const saturation = 70; // Maintains a good amount of color + const lightness = 70; // Increased lightness for a pastel look + + return { h: hue, s: saturation, l: lightness }; +}; + +export const PageEditorBody: React.FC = observer((props) => { + const { + config, + editorForwardRef, + handleConnectionStatus, + handleEditorReady, + handlers, + page, + storeType, + webhookConnectionParams, + projectId, + workspaceSlug, + customRealtimeEventHandlers, + } = props; + // states + const [isDescriptionEmpty, setIsDescriptionEmpty] = useState(false); + const [isVisible, setIsVisible] = useState(false); + const [deletePageModal, setDeletePageModal] = useState({ + visible: false, + pages: [page], + }); + // refs + const titleEditorRef = useRef(null); + // store hooks + const { data: currentUser } = useUser(); + const { + data: { is_smooth_cursor_enabled }, + } = useUserProfile(); + const { getWorkspaceBySlug } = useWorkspace(); + const { getUserDetails } = useMember(); + // derived values + const { id: pageId, isContentEditable, editorRef, setSyncingStatus } = page; + const workspaceId = getWorkspaceBySlug(workspaceSlug)?.id ?? ""; + const isTitleEmpty = !page.name || page.name.trim() === ""; + // Simple animation effect that triggers on mount + useEffect(() => { + const timer = setTimeout(() => { + setIsVisible(true); + }, 100); // Slightly longer delay for smoother coordination + + return () => clearTimeout(timer); + }, []); + + // all editor embeds + const { embedProps } = useEditorEmbeds({ + fetchEmbedSuggestions: handlers.fetchEntity, + getRedirectionLink: handlers.getRedirectionLink, + workspaceSlug, + page, + storeType, + setDeletePageModal, + }); + // use editor mention + const { fetchMentions } = useEditorMention({ + searchEntity: handlers.fetchEntity, + }); + // editor flaggings + const { documentEditor: disabledExtensions } = useEditorFlagging(workspaceSlug?.toString(), storeType); + // page filters + const { fontSize, fontStyle, isFullWidth } = usePageFilters(); + // derived values + const displayConfig: TDisplayConfig = useMemo( + () => ({ + fontSize, + fontStyle, + wideLayout: isFullWidth, + }), + [fontSize, fontStyle, isFullWidth] + ); + + // Use the new hook to handle page events + const { updatePageProperties } = useRealtimePageEvents({ + storeType, + page, + getUserDetails, + customRealtimeEventHandlers, + handlers, + }); + + // Set syncing status on initial render + useEffect(() => { + setSyncingStatus("syncing"); + }, [setSyncingStatus]); + + const getAIMenu = useCallback( + ({ isOpen, onClose }: TAIMenuProps) => ( + + ), + [editorRef, workspaceId, workspaceSlug] + ); + + const handleServerConnect = useCallback(() => { + handleConnectionStatus(false); + }, [handleConnectionStatus]); + + const handleServerError = useCallback(() => { + handleConnectionStatus(true); + setSyncingStatus("error"); + }, [handleConnectionStatus, setSyncingStatus]); + + const handleServerSynced = useCallback(() => { + setSyncingStatus("synced"); + }, [setSyncingStatus]); + + const serverHandler: TServerHandler = useMemo( + () => ({ + onConnect: handleServerConnect, + onServerError: handleServerError, + onServerSynced: handleServerSynced, + }), + [handleServerConnect, handleServerError, handleServerSynced] + ); + + const realtimeConfig: TRealtimeConfig | undefined = useMemo(() => { + // Construct the WebSocket Collaboration URL + try { + const LIVE_SERVER_BASE_URL = LIVE_BASE_URL?.trim() || window.location.origin; + const WS_LIVE_URL = new URL(LIVE_SERVER_BASE_URL); + const isSecureEnvironment = window.location.protocol === "https:"; + WS_LIVE_URL.protocol = isSecureEnvironment ? "wss" : "ws"; + WS_LIVE_URL.pathname = `${LIVE_BASE_PATH}/collaboration`; + // Construct realtime config + return { + url: WS_LIVE_URL.toString(), + queryParams: webhookConnectionParams, + }; + } catch (error) { + console.error("Error creating realtime config", error); + return undefined; + } + }, [webhookConnectionParams]); + + const userConfig = useMemo( + () => ({ + id: currentUser?.id ?? "", + name: currentUser?.display_name ?? "", + color: hslToHex(generateRandomColor(currentUser?.id ?? "")), + }), + [currentUser?.display_name, currentUser?.id] + ); + + const blockWidthClassName = cn( + "block bg-transparent w-full max-w-[720px] mx-auto transition-all duration-200 ease-in-out", + { + "max-w-[1152px]": isFullWidth, + } + ); + + const isPageLoading = pageId === undefined || !realtimeConfig; + if (isPageLoading) return ; + + return ( + +
    + {/* table of content */} +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + { + const res = await fetchMentions(query); + if (!res) throw new Error("Failed in fetching mentions"); + return res; + }, + renderComponent: (props) => , + getMentionedEntityDetails: (id: string) => ({ display_name: getUserDetails(id)?.display_name ?? "" }), + }} + onChange={(_json, html) => { + setIsDescriptionEmpty(isCommentEmpty(html)); + }} + updatePageProperties={updatePageProperties} + embedHandler={embedProps} + pageRestorationInProgress={page.restoration.inProgress} + realtimeConfig={realtimeConfig} + serverHandler={serverHandler} + user={userConfig} + disabledExtensions={disabledExtensions} + aiHandler={{ + menu: getAIMenu, + }} + /> +
    +
    + { + setDeletePageModal({ visible: false, pages: [] }); + }} + pages={deletePageModal.pages} + storeType={storeType} + /> +
    + ); +}); diff --git a/dev-wiki/core/components/pages/editor/header/index.ts b/dev-wiki/core/components/pages/editor/header/index.ts new file mode 100644 index 0000000000..1efe34c51e --- /dev/null +++ b/dev-wiki/core/components/pages/editor/header/index.ts @@ -0,0 +1 @@ +export * from "./root"; diff --git a/dev-wiki/core/components/pages/editor/header/logo-picker.tsx b/dev-wiki/core/components/pages/editor/header/logo-picker.tsx new file mode 100644 index 0000000000..6770466750 --- /dev/null +++ b/dev-wiki/core/components/pages/editor/header/logo-picker.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +// plane imports +import { EmojiIconPicker, EmojiIconPickerTypes } from "@plane/ui"; +import { cn } from "@plane/utils"; +// components +import { Logo } from "@/components/common"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +type Props = { + className?: string; + page: TPageInstance; +}; + +export const PageEditorHeaderLogoPicker: React.FC = observer((props) => { + const { className, page } = props; + // states + const [isLogoPickerOpen, setIsLogoPickerOpen] = useState(false); + // derived values + const { logo_props, isContentEditable, updatePageLogo } = page; + const isLogoSelected = !!logo_props?.in_use; + + return ( +
    + setIsLogoPickerOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" + label={ +
    + {isLogoSelected && } +
    + } + onChange={updatePageLogo} + defaultIconColor={logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined} + defaultOpen={ + logo_props?.in_use && logo_props?.in_use === "emoji" ? EmojiIconPickerTypes.EMOJI : EmojiIconPickerTypes.ICON + } + disabled={!isContentEditable} + /> +
    + ); +}); diff --git a/dev-wiki/core/components/pages/editor/header/root.tsx b/dev-wiki/core/components/pages/editor/header/root.tsx new file mode 100644 index 0000000000..bc9d7fb8b0 --- /dev/null +++ b/dev-wiki/core/components/pages/editor/header/root.tsx @@ -0,0 +1,89 @@ +import { useState } from "react"; +import { observer } from "mobx-react"; +import { SmilePlus } from "lucide-react"; +// plane imports +import type { EditorTitleRefApi } from "@plane/editor"; +import { EmojiIconPicker, EmojiIconPickerTypes } from "@plane/ui"; +import { cn } from "@plane/utils"; +// plane web components +// import { PageTemplatePicker } from "@/plane-web/components/pages"; +// store +import { TPageInstance } from "@/store/pages/base-page"; +// local imports +import { PageEditorHeaderLogoPicker } from "./logo-picker"; + +type Props = { + isEditorContentEmpty: boolean; + isPageLoading: boolean; + page: TPageInstance; + projectId: string | undefined; + titleEditorRef: React.RefObject; + workspaceSlug: string; +}; + +export const PageEditorHeaderRoot: React.FC = observer((props) => { + const { isEditorContentEmpty, isPageLoading, page, projectId, titleEditorRef, workspaceSlug } = props; + // states + const [isLogoPickerOpen, setIsLogoPickerOpen] = useState(false); + // derived values + const { isContentEditable, logo_props, updatePageLogo } = page; + const isLogoSelected = !!logo_props?.in_use; + + return ( + <> +
    +
    + {!isLogoSelected && ( + setIsLogoPickerOpen(val)} + className="flex items-center justify-center" + buttonClassName="flex items-center justify-center" + label={ + + } + onChange={updatePageLogo} + defaultIconColor={ + logo_props?.in_use && logo_props.in_use === "icon" ? logo_props?.icon?.color : undefined + } + defaultOpen={ + logo_props?.in_use && logo_props?.in_use === "emoji" + ? EmojiIconPickerTypes.EMOJI + : EmojiIconPickerTypes.ICON + } + disabled={!isContentEditable} + /> + )} + {/* {isEditorContentEmpty && ( */} + {/* */} + {/* )} */} +
    +
    + + + ); +}); diff --git a/dev-wiki/core/components/pages/editor/index.ts b/dev-wiki/core/components/pages/editor/index.ts new file mode 100644 index 0000000000..83c2ed4e8d --- /dev/null +++ b/dev-wiki/core/components/pages/editor/index.ts @@ -0,0 +1,6 @@ +export * from "./editor-body"; +export * from "./page-root"; +export * from "./breadcrumb-page-item"; +export * from "./summary"; +export * from "./title"; +export * from "./toolbar"; diff --git a/dev-wiki/core/components/pages/editor/page-root.tsx b/dev-wiki/core/components/pages/editor/page-root.tsx new file mode 100644 index 0000000000..f7b9baa13c --- /dev/null +++ b/dev-wiki/core/components/pages/editor/page-root.tsx @@ -0,0 +1,173 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { observer } from "mobx-react"; +import { useSearchParams } from "next/navigation"; +// editor +import { EditorRefApi } from "@plane/editor"; +// types +import { TDocumentPayload, TPage, TPageVersion, TWebhookConnectionQueryParams } from "@plane/types"; +// components +import { setToast, TOAST_TYPE } from "@plane/ui"; +import { + PageEditorToolbarRoot, + PageEditorBody, + PageVersionsOverlay, + PagesVersionEditor, + TEditorBodyHandlers, + TEditorBodyConfig, +} from "@/components/pages"; +// hooks +import { useAppRouter } from "@/hooks/use-app-router"; +import { usePageFallback } from "@/hooks/use-page-fallback"; +import { useQueryParams } from "@/hooks/use-query-params"; +// types +import { TCustomEventHandlers } from "@/hooks/use-realtime-page-events"; +// plane web hooks +import { EPageStoreType, usePageStore } from "@/plane-web/hooks/store/use-page-store"; +// services +import { WorkspacePageVersionService } from "@/plane-web/services/page"; +// store +import { TPageInstance } from "@/store/pages/base-page"; + +export type TPageRootHandlers = { + create: (payload: Partial) => Promise | undefined>; + fetchAllVersions: (pageId: string) => Promise; + fetchDescriptionBinary: () => Promise; + fetchVersionDetails: (pageId: string, versionId: string) => Promise; + updateDescription: (document: TDocumentPayload) => Promise; +} & TEditorBodyHandlers; + +export type TPageRootConfig = TEditorBodyConfig; + +type TPageRootProps = { + config: TPageRootConfig; + handlers: TPageRootHandlers; + page: TPageInstance; + storeType: EPageStoreType; + webhookConnectionParams: TWebhookConnectionQueryParams; + projectId?: string; + workspaceSlug: string; + customRealtimeEventHandlers?: TCustomEventHandlers; +}; + +const workspacePageVersionService = new WorkspacePageVersionService(); + +export const PageRoot = observer((props: TPageRootProps) => { + const { + config, + handlers, + page, + projectId, + storeType, + webhookConnectionParams, + workspaceSlug, + customRealtimeEventHandlers, + } = props; + // states + const [editorReady, setEditorReady] = useState(false); + const [hasConnectionFailed, setHasConnectionFailed] = useState(false); + const [isVersionsOverlayOpen, setIsVersionsOverlayOpen] = useState(false); + // refs + const editorRef = useRef(null); + // router + const router = useAppRouter(); + // search params + const searchParams = useSearchParams(); + // derived values + const { isNestedPagesEnabled } = usePageStore(storeType); + const { isContentEditable, setEditorRef } = page; + // page fallback + usePageFallback({ + editorRef, + fetchPageDescription: handlers.fetchDescriptionBinary, + hasConnectionFailed, + updatePageDescription: handlers.updateDescription, + }); + // update query params + const { updateQueryParams } = useQueryParams(); + + const handleEditorReady = useCallback( + (status: boolean) => { + setEditorReady(status); + if (editorRef.current && !page.editorRef) { + setEditorRef(editorRef.current); + } + }, + [page.editorRef, setEditorRef] + ); + + useEffect(() => { + setTimeout(() => { + setEditorRef(editorRef.current); + }, 0); + }, [isContentEditable, setEditorRef]); + + const version = searchParams.get("version"); + useEffect(() => { + if (!version) { + setIsVersionsOverlayOpen(false); + return; + } + setIsVersionsOverlayOpen(true); + }, [version]); + + const handleCloseVersionsOverlay = () => { + const updatedRoute = updateQueryParams({ + paramsToRemove: ["version"], + }); + router.push(updatedRoute); + }; + + const handleRestoreVersion = async (descriptionHTML: string) => { + if (version && isNestedPagesEnabled(workspaceSlug?.toString())) { + page.setVersionToBeRestored(version, descriptionHTML); + page.setRestorationStatus(true); + setToast({ id: "restoring-version", type: TOAST_TYPE.LOADING_TOAST, title: "Restoring version..." }); + await workspacePageVersionService.restoreVersion(workspaceSlug, page.id ?? "", version ?? ""); + } else { + editorRef.current?.clearEditor(); + editorRef.current?.setEditorValue(descriptionHTML); + } + }; + const currentVersionDescription = editorRef.current?.getDocument().html; + + // reset editor ref on unmount + useEffect( + () => () => { + setEditorRef(null); + }, + [setEditorRef] + ); + + return ( + <> + + + + + ); +}); diff --git a/dev-wiki/core/components/pages/editor/summary/content-browser.tsx b/dev-wiki/core/components/pages/editor/summary/content-browser.tsx new file mode 100644 index 0000000000..e0ef271168 --- /dev/null +++ b/dev-wiki/core/components/pages/editor/summary/content-browser.tsx @@ -0,0 +1,68 @@ +import { useState, useEffect, useCallback } from "react"; +// plane editor +import { EditorRefApi, IMarking } from "@plane/editor"; +// components +import { OutlineHeading1, OutlineHeading2, OutlineHeading3 } from "./heading-components"; + +type Props = { + editorRef: EditorRefApi | null; + setSidePeekVisible?: (sidePeekState: boolean) => void; + showOutline?: boolean; +}; + +export const PageContentBrowser: React.FC = (props) => { + const { editorRef, setSidePeekVisible, showOutline = false } = props; + // states + const [headings, setHeadings] = useState([]); + + useEffect(() => { + const unsubscribe = editorRef?.onHeadingChange(setHeadings); + // for initial render of this component to get the editor headings + setHeadings(editorRef?.getHeadings() ?? []); + return () => { + if (unsubscribe) unsubscribe(); + }; + }, [editorRef]); + + const handleOnClick = useCallback( + (marking: IMarking) => { + editorRef?.scrollSummary(marking); + setSidePeekVisible?.(false); + }, + [editorRef, setSidePeekVisible] + ); + + const HeadingComponent: { + [key: number]: React.FC<{ marking: IMarking; onClick: () => void }>; + } = { + 1: OutlineHeading1, + 2: OutlineHeading2, + 3: OutlineHeading3, + }; + + return ( +
    + {headings.map((marking) => { + const Component = HeadingComponent[marking.level]; + if (!Component) return null; + if (showOutline === true) + return ( +
    + ); + return ( + handleOnClick(marking)} + /> + ); + })} +
    + ); +}; diff --git a/dev-wiki/core/components/pages/editor/summary/heading-components.tsx b/dev-wiki/core/components/pages/editor/summary/heading-components.tsx new file mode 100644 index 0000000000..c2e78dd67f --- /dev/null +++ b/dev-wiki/core/components/pages/editor/summary/heading-components.tsx @@ -0,0 +1,37 @@ +// plane editor +import type { IMarking } from "@plane/editor"; + +export type THeadingComponentProps = { + marking: IMarking; + onClick: (event: React.MouseEvent) => void; +}; + +export const OutlineHeading1 = ({ marking, onClick }: THeadingComponentProps) => ( + +); + +export const OutlineHeading2 = ({ marking, onClick }: THeadingComponentProps) => ( + +); + +export const OutlineHeading3 = ({ marking, onClick }: THeadingComponentProps) => ( + +); diff --git a/dev-wiki/core/components/pages/editor/summary/index.ts b/dev-wiki/core/components/pages/editor/summary/index.ts new file mode 100644 index 0000000000..779b785483 --- /dev/null +++ b/dev-wiki/core/components/pages/editor/summary/index.ts @@ -0,0 +1 @@ +export * from "./content-browser"; diff --git a/dev-wiki/core/components/pages/editor/title.tsx b/dev-wiki/core/components/pages/editor/title.tsx new file mode 100644 index 0000000000..5864ac5d9b --- /dev/null +++ b/dev-wiki/core/components/pages/editor/title.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useState } from "react"; +import { observer } from "mobx-react"; +// editor +import { EditorRefApi } from "@plane/editor"; +// ui +import { TextArea } from "@plane/ui"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { getPageName } from "@/helpers/page.helper"; +// hooks +import { usePageFilters } from "@/hooks/use-page-filters"; + +type Props = { + editorRef: EditorRefApi | null; + readOnly: boolean; + title: string | undefined; + updateTitle: (title: string) => void; +}; + +export const PageEditorTitle: React.FC = observer((props) => { + const { editorRef, readOnly, title, updateTitle } = props; + // states + const [isLengthVisible, setIsLengthVisible] = useState(false); + // page filters + const { fontSize } = usePageFilters(); + // ui + const titleFontClassName = cn("tracking-[-2%] font-bold", { + "text-[1.6rem] leading-[1.9rem]": fontSize === "small-font", + "text-[2rem] leading-[2.375rem]": fontSize === "large-font", + }); + + return ( +
    + {readOnly ? ( +
    + {getPageName(title)} +
    + ) : ( +
    +