diff --git a/packages/i18n/src/locales/en/translations.json b/packages/i18n/src/locales/en/translations.json index 596c3093fa9..d52d125529b 100644 --- a/packages/i18n/src/locales/en/translations.json +++ b/packages/i18n/src/locales/en/translations.json @@ -38,12 +38,10 @@ "deactivate_account_description": "When deactivating an account, all of the data and resources within that account will be permanently removed and cannot be recovered.", "profile_settings": "Profile settings", "your_account": "Your account", - "profile": "Profile", "security": "Security", "activity": "Activity", "appearance": "Appearance", "notifications": "Notifications", - "inbox": "Inbox", "workspaces": "Workspaces", "create_workspace": "Create workspace", "invitations": "Invitations", @@ -155,7 +153,6 @@ "stay_ahead_of_blockers_description": "Spot challenges from one project to another and see inter-cycle dependencies that aren't obvious from any other view.", "analytics": "Analytics", "workspace_invites": "Workspace invites", - "workspace_settings": "Workspace settings", "enter_god_mode": "Enter god mode", "workspace_logo": "Workspace logo", "new_issue": "New issue", @@ -316,5 +313,469 @@ "change_parent_issue": "Change parent issue", "remove_parent_issue": "Remove parent issue", "add_parent": "Add parent", - "loading_members": "Loading members..." + "loading_members": "Loading members...", + "connections": "Connections", + + "workspace_dashboard": { + "empty_state": { + "general": { + "title": "Overview of your projects, activity, and metrics", + "description": "Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", + "primary_button": { + "text": "Build your first project", + "comic": { + "title": "Everything starts with a project in Plane", + "description": "A project could be a product's roadmap, a marketing campaign, or launching a new car." + } + } + } + } + }, + + "workspace_analytics": { + "empty_state": { + "general": { + "title": "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", + "description": "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", + "primary_button": { + "text": "Start your first project", + "comic": { + "title": "Analytics works best with Cycles + Modules", + "description": "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav." + } + } + } + } + }, + + "workspace_projects": { + "empty_state": { + "general": { + "title": "No active projects", + "description": "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal. Create a new project or filter for archived projects.", + "primary_button": { + "text": "Start your first project", + "comic": { + "title": "Everything starts with a project in Plane", + "description": "A project could be a product's roadmap, a marketing campaign, or launching a new car." + } + } + }, + "no_projects": { + "title": "No project", + "description": "To create issues or manage your work, you need to create a project or be a part of one.", + "primary_button": { + "text": "Start your first project", + "comic": { + "title": "Everything starts with a project in Plane", + "description": "A project could be a product's roadmap, a marketing campaign, or launching a new car." + } + } + }, + "filter": { + "title": "No matching projects", + "description": "No projects detected with the matching criteria. \n Create a new project instead." + } + } + }, + + "workspace_issues": { + "empty_state": { + "all-issues": { + "title": "No issues in the project", + "description": "First project done! Now, slice your work into trackable pieces with issues. Let's go!", + "primary_button": { + "text": "Create new issue" + } + }, + "assigned": { + "title": "No issues yet", + "description": "Issues assigned to you can be tracked from here.", + "primary_button": { + "text": "Create new issue" + } + }, + "created": { + "title": "No issues yet", + "description": "All issues created by you come here, track them here directly.", + "primary_button": { + "text": "Create new issue" + } + }, + "subscribed": { + "title": "No issues yet", + "description": "Subscribe to issues you are interested in, track all of them here." + }, + "custom-view": { + "title": "No issues yet", + "description": "Issues that applies to the filters, track all of them here." + } + } + }, + + "workspace_settings": { + "label": "Workspace settings", + "empty_state": { + "api_tokens": { + "title": "No API tokens created", + "description": "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started." + }, + "webhooks": { + "title": "No webhooks added", + "description": "Create webhooks to receive real-time updates and automate actions." + }, + "exports": { + "title": "No exports yet", + "description": "Anytime you export, you will also have a copy here for reference." + }, + "imports": { + "title": "No imports yet", + "description": "Find all your previous imports here and download them." + } + } + }, + + "profile": { + "label": "Profile", + "empty_state": { + "activity": { + "title": "No activities yet", + "description": "Get started by creating a new issue! Add details and properties to it. Explore more in Plane to see your activity." + }, + "assigned": { + "title": "No issues are assigned to you", + "description": "Issues assigned to you can be tracked from here." + }, + "created": { + "title": "No issues yet", + "description": "All issues created by you come here, track them here directly." + }, + "subscribed": { + "title": "No issues yet", + "description": "Subscribe to issues you are interested in, track all of them here." + } + } + }, + + "project_settings": { + "empty_state": { + "labels": { + "title": "No labels yet", + "description": "Create labels to help organize and filter issues in you project." + } + } + }, + + "project_cycles": { + "empty_state": { + "general": { + "title": "Group and timebox your work in Cycles.", + "description": "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", + "primary_button": { + "text": "Set your first cycle", + "comic": { + "title": "Cycles are repetitive time-boxes.", + "description": "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle." + } + } + }, + "no_issues": { + "title": "No issues added to the cycle", + "description": "Add or create issues you wish to timebox and deliver within this cycle", + "primary_button": { + "text": "Create new issue" + }, + "secondary_button": { + "text": "Add existing issue" + } + }, + "completed_no_issues": { + "title": "No issues in the cycle", + "description": "No issues in the cycle. Issues are either transferred or hidden. To see hidden issues if any, update your display properties accordingly." + }, + "active": { + "title": "No active cycle", + "description": "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here." + }, + "archived": { + "title": "No archived cycles yet", + "description": "To tidy up your project, archive completed cycles. Find them here once archived." + } + } + }, + + "project_issues": { + "empty_state": { + "no_issues": { + "title": "Create an issue and assign it to someone, even yourself", + "description": "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", + "primary_button": { + "text": "Create your first issue", + "comic": { + "title": "Issues are building blocks in Plane.", + "description": "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues." + } + } + }, + "no_archived_issues": { + "title": "No archived issues yet", + "description": "Manually or through automation, you can archive issues that are completed or cancelled. Find them here once archived.", + "primary_button": { + "text": "Set automation" + } + }, + "issues_empty_filter": { + "title": "No issues found matching the filters applied", + "secondary_button": { + "text": "Clear all filters" + } + } + } + }, + + "project_module": { + "empty_state": { + "general": { + "title": "Map your project milestones to Modules and track aggregated work easily.", + "description": "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", + "primary_button": { + "text": "Build your first module", + "comic": { + "title": "Modules help group work by hierarchy.", + "description": "A cart module, a chassis module, and a warehouse module are all good example of this grouping." + } + } + }, + "no_issues": { + "title": "No issues in the module", + "description": "Create or add issues which you want to accomplish as part of this module", + "primary_button": { + "text": "Create new issue" + }, + "secondary_button": { + "text": "Add an existing issue" + } + }, + "archived": { + "title": "No archived Modules yet", + "description": "To tidy up your project, archive completed or cancelled modules. Find them here once archived." + } + } + }, + + "project_views": { + "empty_state": { + "general": { + "title": "Save filtered views for your project. Create as many as you need", + "description": "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", + "primary_button": { + "text": "Create your first view", + "comic": { + "title": "Views work atop Issue properties.", + "description": "You can create a view from here with as many properties as filters as you see fit." + } + } + }, + "filter": { + "title": "No matching views", + "description": "No views match the search criteria. \n Create a new view instead." + } + } + }, + + "project_page": { + "empty_state": { + "general": { + "title": "Write a note, a doc, or a full knowledge base. Get Galileo, Plane's AI assistant, to help you get started", + "description": "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project's context. To make short work of any doc, invoke Galileo, Plane's AI, with a shortcut or the click of a button.", + "primary_button": { + "text": "Create your first page" + } + }, + "private": { + "title": "No private pages yet", + "description": "Keep your private thoughts here. When you're ready to share, the team's just a click away.", + "primary_button": { + "text": "Create your first page" + } + }, + "public": { + "title": "No public pages yet", + "description": "See pages shared with everyone in your project right here.", + "primary_button": { + "text": "Create your first page" + } + }, + "archived": { + "title": "No archived pages yet", + "description": "Archive pages not on your radar. Access them here when needed." + } + } + }, + + "command_k": { + "empty_state": { + "search": { + "title": "No results found" + } + } + }, + + "issue_relation": { + "empty_state": { + "search": { + "title": "No matching issues found" + }, + "no_issues": { + "title": "No issues found" + } + } + }, + + "issue_comment": { + "empty_state": { + "general": { + "title": "No comments yet", + "description": "Comments can be used as a discussion and follow-up space for the issues" + } + } + }, + + "notification": { + "empty_state": { + "detail": { + "title": "Select to view details." + }, + "all": { + "title": "No issues assigned", + "description": "Updates for issues assigned to you can be \n seen here" + }, + "mentions": { + "title": "No issues assigned", + "description": "Updates for issues assigned to you can be \n seen here" + } + } + }, + + "active_cycle": { + "empty_state": { + "progress": { + "title": "Add issues to the cycle to view it's progress" + }, + "chart": { + "title": "Add issues to the cycle to view the burndown chart." + }, + "priority_issue": { + "title": "Observe high priority issues tackled in the cycle at a glance." + }, + "assignee": { + "title": "Add assignees to issues to see a breakdown of work by assignees." + }, + "label": { + "title": "Add labels to issues to see the breakdown of work by labels." + } + } + }, + + "disabled_project": { + "empty_state": { + "inbox": { + "title": "Intake is not enabled for the project.", + "description": "Intake helps you manage incoming requests to your project and add them as issues in your workflow. Enable intake from project settings to manage requests.", + "primary_button": { + "text": "Manage features" + } + }, + "cycle": { + "title": "Cycles is not enabled for this project.", + "description": "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team. Enable the cycles feature for your project to start using them.", + "primary_button": { + "text": "Manage features" + } + }, + "module": { + "title": "Modules are not enabled for the project.", + "description": "Modules are the building blocks of your project. Enable modules from project settings to start using them.", + "primary_button": { + "text": "Manage features" + } + }, + "page": { + "title": "Pages are not enabled for the project.", + "description": "Pages are the building blocks of your project. Enable pages from project settings to start using them.", + "primary_button": { + "text": "Manage features" + } + }, + "view": { + "title": "Views are not enabled for the project.", + "description": "Views are the building blocks of your project. Enable views from project settings to start using them.", + "primary_button": { + "text": "Manage features" + } + } + } + }, + + "inbox": { + "label": "Inbox", + "empty_state": { + "sidebar_open_tab": { + "title": "No open issues", + "description": "Find open issues here. Create new issue." + }, + "sidebar_closed_tab": { + "title": "No closed issues", + "description": "All the issues whether accepted or declined can be found here." + }, + "sidebar_filter": { + "title": "No matching issues", + "description": "No issue matches filter applied in intake. Create a new issue." + }, + "detail": { + "title": "Select an issue to view its details." + } + } + }, + + "workspace_draft_issues": { + "empty_state": { + "title": "Half-written issues, and soon, comments will show up here.", + "description": "To try this out, start adding an issue and leave it mid-way or create your first draft below. 😉", + "primary_button": { + "text": "Create your first draft" + } + } + }, + + "stickies": { + "empty_state": { + "general": { + "title": "Stickies are quick notes and to-dos you take down on the fly.", + "description": "Capture your thoughts and ideas effortlessly by creating stickies that you can access anytime and from anywhere.", + "primary_button": { + "text": "Add sticky" + } + }, + "search": { + "title": "That doesn't match any of your stickies.", + "description": "Try a different term or let us know\nif you are sure your search is right. ", + "primary_button": { + "text": "Add sticky" + } + } + } + }, + + "home_widgets": { + "empty_state": { + "general": { + "title": "It's Quiet Without Widgets, Turn Them On", + "description": "It looks like all your widgets are turned off. Enable them\nnow to enhance your experience!", + "primary_button": { + "text": "Manage widgets" + } + } + } + } } diff --git a/packages/i18n/src/locales/es/translations.json b/packages/i18n/src/locales/es/translations.json index 9f2b98792c2..f516cf152d6 100644 --- a/packages/i18n/src/locales/es/translations.json +++ b/packages/i18n/src/locales/es/translations.json @@ -38,7 +38,6 @@ "deactivate_account_description": "Al desactivar una cuenta, todos los datos y recursos dentro de esa cuenta se eliminarán permanentemente y no se podrán recuperar.", "profile_settings": "Configuración de perfil", "your_account": "Tu cuenta", - "profile": "Perfil", "security": "Seguridad", "activity": "Actividad", "appearance": "Apariencia", @@ -114,7 +113,6 @@ "plane_logo": "Logo de Plane", "workspace_creation_disabled": "Creación de espacio de trabajo deshabilitada", "workspace_request_subject": "Solicitando un nuevo espacio de trabajo", - "workspace_request_body": "Hola administrador(es) de instancia,\n\nPor favor, crea un nuevo espacio de trabajo con la URL [/nombre-del-espacio-de-trabajo] para [propósito de crear el espacio de trabajo].\n\nGracias,\n{{firstName}} {{lastName}}\n{{email}}", "creating_workspace": "Creando espacio de trabajo", "workspace_created_successfully": "Espacio de trabajo creado con éxito", "create_workspace_page": "Página de creación de espacio de trabajo", @@ -154,7 +152,6 @@ "stay_ahead_of_blockers_description": "Detecta desafíos de un proyecto a otro y ve dependencias entre ciclos que no son obvias desde ninguna otra vista.", "analytics": "Analítica", "workspace_invites": "Invitaciones al espacio de trabajo", - "workspace_settings": "Configuración del espacio de trabajo", "enter_god_mode": "Entrar en modo dios", "workspace_logo": "Logo del espacio de trabajo", "new_issue": "Nuevo problema", @@ -316,5 +313,468 @@ "remove_parent_issue": "Eliminar problema padre", "add_parent": "Agregar padre", "loading_members": "Cargando miembros...", - "inbox": "bandeja de entrada" + "connections": "Conexiones", + + "workspace_dashboard": { + "empty_state": { + "general": { + "title": "Resumen de tus proyectos, actividad y métricas", + "description": "Bienvenido a Plane, estamos emocionados de tenerte aquí. Crea tu primer proyecto y rastrea tus problemas, y esta página se transformará en un espacio que te ayudará a avanzar. Los administradores también verán elementos que ayudan a su equipo a progresar.", + "primary_button": { + "text": "Construye tu primer proyecto", + "comic": { + "title": "Todo comienza con un proyecto en Plane", + "description": "Un proyecto podría ser la hoja de ruta de un producto, una campaña de marketing o el lanzamiento de un nuevo coche." + } + } + } + } + }, + + "workspace_analytics": { + "empty_state": { + "general": { + "title": "Rastrea el progreso, cargas de trabajo y asignaciones. Detecta tendencias, elimina bloqueos y acelera el trabajo", + "description": "Consulta el alcance frente a la demanda, estimaciones y desbordamiento del alcance. Obtén el rendimiento por miembros del equipo y equipos, y asegúrate de que tu proyecto termine a tiempo.", + "primary_button": { + "text": "Comienza tu primer proyecto", + "comic": { + "title": "La analítica funciona mejor con Ciclos + Módulos", + "description": "Primero, organiza tus problemas en ciclos y, si puedes, agrupa problemas que abarquen más de un ciclo en módulos. Consulta ambos en el menú de navegación a la izquierda." + } + } + } + } + }, + + "workspace_projects": { + "empty_state": { + "general": { + "title": "No hay proyectos activos", + "description": "Piensa en cada proyecto como el contenedor para trabajo orientado a objetivos. Los proyectos son donde viven Trabajos, Ciclos y Módulos y, junto con tus colegas, te ayudan a lograr ese objetivo. Crea un nuevo proyecto o filtra por proyectos archivados.", + "primary_button": { + "text": "Comienza tu primer proyecto", + "comic": { + "title": "Todo comienza con un proyecto en Plane", + "description": "Un proyecto podría ser la hoja de ruta de un producto, una campaña de marketing o el lanzamiento de un nuevo coche." + } + } + }, + "no_projects": { + "title": "Sin proyectos", + "description": "Para crear problemas o gestionar tu trabajo, necesitas crear un proyecto o ser parte de uno.", + "primary_button": { + "text": "Comienza tu primer proyecto", + "comic": { + "title": "Todo comienza con un proyecto en Plane", + "description": "Un proyecto podría ser la hoja de ruta de un producto, una campaña de marketing o el lanzamiento de un nuevo coche." + } + } + }, + "filter": { + "title": "No hay proyectos coincidentes", + "description": "No se detectaron proyectos con los criterios coincidentes. \n Crea un nuevo proyecto en su lugar." + } + } + }, + + "workspace_issues": { + "empty_state": { + "all-issues": { + "title": "No hay problemas en el proyecto", + "description": "¡Primer proyecto terminado! Ahora, divide tu trabajo en piezas rastreables con problemas. ¡Vamos!", + "primary_button": { + "text": "Crear nuevo problema" + } + }, + "assigned": { + "title": "Aún no hay problemas", + "description": "Los problemas asignados a ti se pueden rastrear desde aquí.", + "primary_button": { + "text": "Crear nuevo problema" + } + }, + "created": { + "title": "Aún no hay problemas", + "description": "Todos los problemas creados por ti aparecen aquí. Rastréalos directamente.", + "primary_button": { + "text": "Crear nuevo problema" + } + }, + "subscribed": { + "title": "Aún no hay problemas", + "description": "Suscríbete a los problemas que te interesan y rastrea todos ellos aquí." + }, + "custom-view": { + "title": "Aún no hay problemas", + "description": "Los problemas que se aplican a los filtros se rastrean aquí." + } + } + }, + + "workspace_settings": { + "label": "Configuración del espacio de trabajo", + "empty_state": { + "api_tokens": { + "title": "No se han creado tokens API", + "description": "Las APIs de Plane pueden ser usadas para integrar tus datos en Plane con cualquier sistema externo. Crea un token para comenzar." + }, + "webhooks": { + "title": "No se han agregado webhooks", + "description": "Crea webhooks para recibir actualizaciones en tiempo real y automatizar acciones." + }, + "exports": { + "title": "Aún no hay exportaciones", + "description": "Cada vez que exportes, también tendrás una copia aquí para referencia." + }, + "imports": { + "title": "Aún no hay importaciones", + "description": "Encuentra todas tus importaciones previas aquí y descárgalas." + } + } + }, + + "profile": { + "label": "Perfil", + "empty_state": { + "activity": { + "title": "Aún no hay actividades", + "description": "¡Comienza creando un nuevo problema! Añade detalles y propiedades a él. Explora más en Plane para ver tu actividad." + }, + "assigned": { + "title": "No tienes problemas asignados", + "description": "Los problemas asignados a ti se pueden rastrear desde aquí." + }, + "created": { + "title": "Aún no hay problemas", + "description": "Todos los problemas creados por ti aparecen aquí. Rastréalos directamente." + }, + "subscribed": { + "title": "Aún no hay problemas", + "description": "Suscríbete a los problemas que te interesan y rastrea todos ellos aquí." + } + } + }, + + "project_settings": { + "empty_state": { + "labels": { + "title": "Aún no hay etiquetas", + "description": "Crea etiquetas para ayudar a organizar y filtrar problemas en tu proyecto." + } + } + }, + + "project_cycles": { + "empty_state": { + "general": { + "title": "Agrupa y organiza tu trabajo en Ciclos.", + "description": "Divide el trabajo en partes organizadas por plazos, retrocede desde la fecha límite de tu proyecto para establecer fechas y haz un progreso tangible como equipo.", + "primary_button": { + "text": "Configura tu primer ciclo", + "comic": { + "title": "Los ciclos son bloques de tiempo repetitivos.", + "description": "Un sprint, una iteración o cualquier otro término que uses para el seguimiento semanal o quincenal del trabajo es un ciclo." + } + } + }, + "no_issues": { + "title": "No hay problemas añadidos al ciclo", + "description": "Añade o crea problemas que desees organizar y entregar dentro de este ciclo", + "primary_button": { + "text": "Crear nuevo problema" + }, + "secondary_button": { + "text": "Añadir problema existente" + } + }, + "completed_no_issues": { + "title": "No hay problemas en el ciclo", + "description": "No hay problemas en el ciclo. Los problemas se han transferido o están ocultos. Para ver problemas ocultos, si los hay, actualiza las propiedades de visualización en consecuencia." + }, + "active": { + "title": "No hay ciclo activo", + "description": "Un ciclo activo incluye cualquier período que abarque la fecha de hoy dentro de su rango. Encuentra el progreso y los detalles del ciclo activo aquí." + }, + "archived": { + "title": "No hay ciclos archivados todavía", + "description": "Para organizar tu proyecto, archiva ciclos completados. Encuéntralos aquí una vez archivados." + } + } + }, + + "project_issues": { + "empty_state": { + "no_issues": { + "title": "Crea un problema y asígnalo a alguien, incluso a ti mismo", + "description": "Piensa en los problemas como trabajos, tareas, acciones, o JTBD (trabajos por hacer). Nos gusta eso. Un problema y sus subproblemas suelen ser acciones basadas en el tiempo asignadas a los miembros de tu equipo. Tu equipo crea, asigna y completa problemas para avanzar el proyecto hacia su objetivo.", + "primary_button": { + "text": "Crea tu primer problema", + "comic": { + "title": "Los problemas son bloques de construcción en Plane.", + "description": "Rediseñar la interfaz de Plane, Rebrandear la empresa o Lanzar el nuevo sistema de inyección de combustible son ejemplos de problemas que probablemente tengan subproblemas." + } + } + }, + "no_archived_issues": { + "title": "Aún no hay problemas archivados", + "description": "De forma manual o mediante automatización, puedes archivar problemas que estén completados o cancelados. Encuéntralos aquí una vez archivados.", + "primary_button": { + "text": "Configurar automatización" + } + }, + "issues_empty_filter": { + "title": "No se encontraron problemas que coincidan con los filtros aplicados", + "secondary_button": { + "text": "Borrar todos los filtros" + } + } + } + }, + + "project_module": { + "empty_state": { + "no_issues": { + "title": "No hay problemas en el módulo", + "description": "Crea o agrega problemas que deseas lograr como parte de este módulo", + "primary_button": { + "text": "Crear nuevo problema" + }, + "secondary_button": { + "text": "Agregar un problema existente" + } + }, + "general": { + "title": "Mapea los hitos de tu proyecto a Módulos y sigue el trabajo agregado fácilmente.", + "description": "Un grupo de problemas que pertenecen a un padre lógico y jerárquico forma un módulo. Piénsalos como una forma de rastrear el trabajo por hitos del proyecto. Tienen sus propios períodos y plazos, así como análisis para ayudarte a ver qué tan cerca o lejos estás de un hito.", + "primary_button": { + "text": "Construye tu primer módulo", + "comic": { + "title": "Los módulos ayudan a agrupar el trabajo por jerarquía.", + "description": "Un módulo de carrito, un módulo de chasis y un módulo de almacén son buenos ejemplos de esta agrupación." + } + } + }, + "archived": { + "title": "Aún no hay módulos archivados", + "description": "Para organizar tu proyecto, archiva módulos completados o cancelados. Encuéntralos aquí una vez archivados." + } + } + }, + + "project_views": { + "empty_state": { + "general": { + "title": "Guarda vistas filtradas para tu proyecto. Crea tantas como necesites", + "description": "Las vistas son un conjunto de filtros guardados que usas con frecuencia o a los que deseas tener acceso fácil. Todos tus colegas en un proyecto pueden ver las vistas de todos y elegir la que mejor se adapte a sus necesidades.", + "primary_button": { + "text": "Crea tu primera vista", + "comic": { + "title": "Las vistas funcionan sobre las propiedades de los problemas.", + "description": "Puedes crear una vista desde aquí con tantas propiedades como filtros que consideres necesarios." + } + } + }, + "filter": { + "title": "No hay vistas coincidentes", + "description": "Ninguna vista coincide con los criterios de búsqueda. \n Crea una nueva vista en su lugar." + } + } + }, + + "project_page": { + "empty_state": { + "general": { + "title": "Escribe una nota, un documento o una base de conocimiento completa. Deja que Galileo, el asistente de IA de Plane, te ayude a empezar", + "description": "Las páginas son un espacio para capturar ideas en Plane. Toma notas de reuniones, formatearlas fácilmente, incrusta problemas, organiza usando una biblioteca de componentes y mantén todo en el contexto de tu proyecto. Para simplificar cualquier documento, invoca a Galileo, la IA de Plane, con un atajo o un clic.", + "primary_button": { + "text": "Crea tu primera página" + } + }, + "private": { + "title": "Aún no hay páginas privadas", + "description": "Guarda aquí tus pensamientos privados. Cuando estés listo para compartirlos, el equipo está a un clic de distancia.", + "primary_button": { + "text": "Crea tu primera página" + } + }, + "public": { + "title": "Aún no hay páginas públicas", + "description": "Consulta aquí las páginas compartidas con todos en tu proyecto.", + "primary_button": { + "text": "Crea tu primera página" + } + }, + "archived": { + "title": "Aún no hay páginas archivadas", + "description": "Archiva las páginas que no están en tu radar. Accede a ellas aquí cuando las necesites." + } + } + }, + + "command_k": { + "empty_state": { + "search": { + "title": "No se encontraron resultados" + } + } + }, + + "issue_relation": { + "empty_state": { + "search": { + "title": "No se encontraron problemas coincidentes" + }, + "general": { + "title": "No se encontraron problemas" + } + } + }, + + "issue_comment": { + "empty_state": { + "general": { + "title": "Aún no hay comentarios", + "description": "Los comentarios pueden usarse como un espacio de discusión y seguimiento para los problemas" + } + } + }, + + "notification": { + "empty_state": { + "detail": { + "title": "Selecciona para ver los detalles." + }, + "all": { + "title": "No hay problemas asignados", + "description": "Las actualizaciones de los problemas asignados a ti se \n pueden ver aquí" + }, + "mentions": { + "title": "No hay problemas asignados", + "description": "Las actualizaciones de los problemas asignados a ti se \n pueden ver aquí" + } + } + }, + + "active_cycle": { + "empty_state": { + "progress": { + "title": "Agrega problemas al ciclo para ver su progreso." + }, + "chart": { + "title": "Agrega problemas al ciclo para ver el gráfico de burndown." + }, + "priority_issue": { + "title": "Observa de un vistazo los problemas de alta prioridad abordados en el ciclo." + }, + "assignee": { + "title": "Asigna responsables a los problemas para ver un desglose del trabajo por responsables." + }, + "label": { + "title": "Agrega etiquetas a los problemas para ver el desglose del trabajo por etiquetas." + } + } + }, + + "disabled_project": { + "empty_state": { + "inbox": { + "title": "La entrada no está habilitada para el proyecto.", + "description": "La entrada te ayuda a gestionar las solicitudes entrantes para tu proyecto y agregarlas como problemas en tu flujo de trabajo. Habilita la entrada desde la configuración del proyecto para gestionar solicitudes.", + "primary_button": { + "text": "Gestionar funciones" + } + }, + "cycle": { + "title": "Los ciclos no están habilitados para este proyecto.", + "description": "Divide el trabajo en bloques de tiempo, trabaja hacia atrás desde la fecha límite de tu proyecto para establecer fechas y logra avances tangibles como equipo. Habilita la función de ciclos para tu proyecto para comenzar a usarlos.", + "primary_button": { + "text": "Gestionar funciones" + } + }, + "module": { + "title": "Los módulos no están habilitados para el proyecto.", + "description": "Los módulos son los bloques de construcción de tu proyecto. Habilita módulos desde la configuración del proyecto para comenzar a usarlos.", + "primary_button": { + "text": "Gestionar funciones" + } + }, + "page": { + "title": "Las páginas no están habilitadas para el proyecto.", + "description": "Las páginas son los bloques de construcción de tu proyecto. Habilita páginas desde la configuración del proyecto para comenzar a usarlas.", + "primary_button": { + "text": "Gestionar funciones" + } + }, + "view": { + "title": "Las vistas no están habilitadas para el proyecto.", + "description": "Las vistas son los bloques de construcción de tu proyecto. Habilita vistas desde la configuración del proyecto para comenzar a usarlas.", + "primary_button": { + "text": "Gestionar funciones" + } + } + } + }, + + "inbox": { + "label": "Bandeja de entrada", + "empty_state": { + "sidebar_open_tab": { + "title": "No hay problemas abiertos", + "description": "Encuentra los problemas abiertos aquí. Crea un nuevo problema." + }, + "sidebar_closed_tab": { + "title": "No hay problemas cerrados", + "description": "Todos los problemas, ya sean aceptados o rechazados, se pueden encontrar aquí." + }, + "sidebar_filter": { + "title": "No hay problemas coincidentes", + "description": "Ningún problema coincide con el filtro aplicado en la entrada. Crea un nuevo problema." + }, + "detail": { + "title": "Selecciona un problema para ver sus detalles." + } + } + }, + + "workspace_draft_issues": { + "empty_state": { + "title": "Los problemas a medio escribir, y pronto, los comentarios aparecerán aquí.", + "description": "Para probar esto, comienza a agregar un problema y déjalo a medias o crea tu primer borrador abajo. 😉", + "primary_button": { + "text": "Crea tu primer borrador" + } + } + }, + + "stickies": { + "empty_state": { + "general": { + "title": "Los stickies son notas rápidas y tareas que tomas al vuelo.", + "description": "Captura tus pensamientos e ideas sin esfuerzo creando stickies a los que puedes acceder en cualquier momento y lugar.", + "primary_button": { + "text": "Agregar sticky" + } + }, + "search": { + "title": "Eso no coincide con ninguno de tus stickies.", + "description": "Prueba con un término diferente o avísanos si estás seguro de que tu búsqueda es correcta.", + "primary_button": { + "text": "Agregar sticky" + } + } + } + }, + + "home_widgets": { + "empty_state": { + "general": { + "title": "Está tranquilo sin widgets, actívalos", + "description": "Parece que todos tus widgets están desactivados. ¡Enciéndelos ahora para mejorar tu experiencia!", + "primary_button": { + "text": "Gestionar widgets" + } + } + } + } } diff --git a/packages/i18n/src/locales/fr/translations.json b/packages/i18n/src/locales/fr/translations.json index 0eee868e174..4d5d7512efd 100644 --- a/packages/i18n/src/locales/fr/translations.json +++ b/packages/i18n/src/locales/fr/translations.json @@ -38,7 +38,6 @@ "deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées.", "profile_settings": "Paramètres du profil", "your_account": "Votre compte", - "profile": "Profil", "security": " Sécurité", "activity": "Activité", "appearance": "Apparence", @@ -154,7 +153,6 @@ "stay_ahead_of_blockers_description": "Repérez les défis d'un projet à l'autre et identifiez les dépendances entre cycles qui ne sont pas évidentes depuis d'autres vues.", "analytics": "Analyse", "workspace_invites": "Invitations de l'espace de travail", - "workspace_settings": "Paramètres de l'espace de travail", "enter_god_mode": "Entrer en mode dieu", "workspace_logo": "Logo de l'espace de travail", "new_issue": "Nouveau problème", @@ -316,5 +314,468 @@ "remove_parent_issue": "Supprimer le problème parent", "add_parent": "Ajouter un parent", "loading_members": "Chargement des membres...", - "inbox": "Boîte de réception" + "connections": "Connexions", + + "workspace_dashboard": { + "empty_state": { + "general": { + "title": "Vue d'ensemble de vos projets, activités et métriques", + "description": "Bienvenue sur Plane, nous sommes ravis de vous accueillir. Créez votre premier projet et suivez vos tâches, et cette page se transformera en un espace qui vous aidera à progresser. Les administrateurs verront également des éléments pour aider leur équipe à progresser.", + "primary_button": { + "text": "Créez votre premier projet", + "comic": { + "title": "Tout commence par un projet sur Plane", + "description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture." + } + } + } + } + }, + + "workspace_analytics": { + "empty_state": { + "general": { + "title": "Suivez les progrès, les charges de travail et les allocations. Repérez les tendances, éliminez les obstacles et accélérez le travail", + "description": "Visualisez l'étendue par rapport à la demande, les estimations et l'expansion des objectifs. Obtenez des performances par membre et par équipe, et assurez-vous que votre projet respecte les délais.", + "primary_button": { + "text": "Commencez votre premier projet", + "comic": { + "title": "Les analyses fonctionnent mieux avec Cycles + Modules", + "description": "D'abord, cadrez vos tâches dans des Cycles et, si possible, regroupez les tâches qui s'étendent sur plus d'un cycle dans des Modules. Consultez-les dans la navigation à gauche." + } + } + } + } + }, + + "workspace_projects": { + "empty_state": { + "general": { + "title": "Aucun projet actif", + "description": "Considérez chaque projet comme le parent des travaux orientés vers un objectif. Les projets sont l'endroit où vivent les tâches, les Cycles et les Modules, et avec vos collègues, ils vous aident à atteindre cet objectif. Créez un nouveau projet ou filtrez les projets archivés.", + "primary_button": { + "text": "Commencez votre premier projet", + "comic": { + "title": "Tout commence par un projet sur Plane", + "description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture." + } + } + }, + "no_projects": { + "title": "Aucun projet", + "description": "Pour créer des tâches ou gérer votre travail, vous devez créer un projet ou en faire partie.", + "primary_button": { + "text": "Commencez votre premier projet", + "comic": { + "title": "Tout commence par un projet sur Plane", + "description": "Un projet peut être une feuille de route produit, une campagne marketing ou le lancement d'une nouvelle voiture." + } + } + }, + "filter": { + "title": "Aucun projet correspondant", + "description": "Aucun projet détecté avec les critères correspondants. \n Créez un nouveau projet à la place." + } + } + }, + + "workspace_issues": { + "empty_state": { + "all-issues": { + "title": "Aucune tâche dans le projet", + "description": "Premier projet terminé ! Maintenant, divisez votre travail en morceaux traçables avec des tâches. Allons-y !", + "primary_button": { + "text": "Créer une nouvelle tâche" + } + }, + "assigned": { + "title": "Aucune tâche assignée", + "description": "Les tâches qui vous sont assignées peuvent être suivies ici.", + "primary_button": { + "text": "Créer une nouvelle tâche" + } + }, + "created": { + "title": "Aucune tâche créée", + "description": "Toutes les tâches que vous avez créées se trouvent ici. Suivez-les directement ici.", + "primary_button": { + "text": "Créer une nouvelle tâche" + } + }, + "subscribed": { + "title": "Aucune tâche suivie", + "description": "Abonnez-vous aux tâches qui vous intéressent, suivez-les toutes ici." + }, + "custom-view": { + "title": "Aucune tâche trouvée", + "description": "Les tâches correspondant aux filtres sont affichées ici. Suivez-les toutes ici." + } + } + }, + + "workspace_settings": { + "label": "Paramètres de l'espace de travail", + "empty_state": { + "api_tokens": { + "title": "Aucun jeton API créé", + "description": "Les API Plane peuvent être utilisées pour intégrer vos données dans Plane avec n'importe quel système externe. Créez un jeton pour commencer." + }, + "webhooks": { + "title": "Aucun webhook ajouté", + "description": "Créez des webhooks pour recevoir des mises à jour en temps réel et automatiser des actions." + }, + "exports": { + "title": "Aucune exportation pour le moment", + "description": "Chaque fois que vous exportez, une copie sera également disponible ici pour référence." + }, + "imports": { + "title": "Aucune importation pour le moment", + "description": "Retrouvez toutes vos importations précédentes ici et téléchargez-les." + } + } + }, + + "profile": { + "label": "Profil", + "empty_state": { + "activity": { + "title": "Aucune activité pour le moment", + "description": "Commencez par créer une nouvelle tâche ! Ajoutez des détails et des propriétés à celle-ci. Explorez davantage Plane pour voir votre activité." + }, + "assigned": { + "title": "Aucune tâche assignée", + "description": "Les tâches qui vous sont assignées peuvent être suivies ici." + }, + "created": { + "title": "Aucune tâche créée", + "description": "Toutes les tâches que vous avez créées se trouvent ici. Suivez-les directement ici." + }, + "subscribed": { + "title": "Aucune tâche suivie", + "description": "Abonnez-vous aux tâches qui vous intéressent, suivez-les toutes ici." + } + } + }, + + "project_settings": { + "empty_state": { + "labels": { + "title": "Aucune étiquette pour le moment", + "description": "Créez des étiquettes pour organiser et filtrer les tâches de votre projet." + } + } + }, + + "project_cycles": { + "empty_state": { + "general": { + "title": "Groupez et cadrez votre travail en cycles.", + "description": "Décomposez le travail en périodes définies, travaillez à rebours à partir de votre date limite pour fixer des échéances, et réalisez des progrès tangibles en équipe.", + "primary_button": { + "text": "Définir votre premier cycle", + "comic": { + "title": "Les cycles sont des périodes répétitives.", + "description": "Un sprint, une itération ou tout autre terme que vous utilisez pour le suivi hebdomadaire ou bi-hebdomadaire du travail est un cycle." + } + } + }, + "no_issues": { + "title": "Aucune tâche ajoutée au cycle", + "description": "Ajoutez ou créez des tâches que vous souhaitez cadencer et livrer dans ce cycle", + "primary_button": { + "text": "Créer une nouvelle tâche" + }, + "secondary_button": { + "text": "Ajouter une tâche existante" + } + }, + "completed_no_issues": { + "title": "Aucun problème dans le cycle", + "description": "Aucun problème dans le cycle. Les problèmes ont été transférés ou sont cachés. Pour voir les problèmes cachés, le cas échéant, mettez à jour les propriétés d'affichage en conséquence." + }, + "active": { + "title": "Aucun cycle actif", + "description": "Un cycle actif inclut toute période englobant la date d'aujourd'hui dans sa plage. Retrouvez ici les progrès et les détails du cycle actif." + }, + "archived": { + "title": "Aucun cycle archivé pour le moment", + "description": "Pour organiser votre projet, archivez les cycles terminés. Retrouvez-les ici une fois archivés." + } + } + }, + + "project_issues": { + "empty_state": { + "no_issues": { + "title": "Créez un problème et assignez-le à quelqu’un, même à vous-même", + "description": "Considérez les problèmes comme des emplois, des tâches, des actions ou JTBD (travaux à accomplir). Nous aimons ça. Un problème et ses sous-problèmes sont généralement des actions basées sur le temps assignées aux membres de votre équipe. Votre équipe crée, assigne et termine des problèmes pour faire avancer votre projet vers son objectif.", + "primary_button": { + "text": "Créez votre premier problème", + "comic": { + "title": "Les problèmes sont les éléments constitutifs de Plane.", + "description": "Redessiner l’interface utilisateur de Plane, Rebrander l’entreprise ou Lancer le nouveau système d’injection de carburant sont des exemples de problèmes qui comportent probablement des sous-problèmes." + } + } + }, + "no_archived_issues": { + "title": "Aucun problème archivé pour l'instant", + "description": "Manuellement ou via une automatisation, vous pouvez archiver les problèmes terminés ou annulés. Retrouvez-les ici une fois archivés.", + "primary_button": { + "text": "Configurer l'automatisation" + } + }, + "issues_empty_filter": { + "title": "Aucun problème trouvé correspondant aux filtres appliqués", + "secondary_button": { + "text": "Effacer tous les filtres" + } + } + } + }, + + "project_module": { + "empty_state": { + "no_issues": { + "title": "Aucun problème dans le module", + "description": "Créez ou ajoutez des problèmes que vous souhaitez réaliser dans le cadre de ce module", + "primary_button": { + "text": "Créer un nouveau problème" + }, + "secondary_button": { + "text": "Ajouter un problème existant" + } + }, + "general": { + "title": "Associez les jalons de votre projet aux modules et suivez facilement le travail global.", + "description": "Un groupe de problèmes appartenant à un parent logique et hiérarchique forme un module. Pensez-y comme un moyen de suivre le travail par jalons de projet. Ils ont leurs propres périodes, échéances et analyses pour vous aider à voir votre progression vers un jalon.", + "primary_button": { + "text": "Construisez votre premier module", + "comic": { + "title": "Les modules aident à regrouper le travail par hiérarchie.", + "description": "Un module de panier, un module de châssis et un module d'entrepôt sont de bons exemples de ce regroupement." + } + } + }, + "archived": { + "title": "Aucun module archivé pour le moment", + "description": "Pour organiser votre projet, archivez les modules terminés ou annulés. Retrouvez-les ici une fois archivés." + } + } + }, + + "project_views": { + "empty_state": { + "general": { + "title": "Enregistrez des vues filtrées pour votre projet. Créez-en autant que nécessaire", + "description": "Les vues sont un ensemble de filtres enregistrés que vous utilisez fréquemment ou auxquels vous souhaitez accéder facilement. Tous vos collègues dans un projet peuvent voir les vues de chacun et choisir celle qui correspond le mieux à leurs besoins.", + "primary_button": { + "text": "Créez votre première vue", + "comic": { + "title": "Les vues fonctionnent au-dessus des propriétés des problèmes.", + "description": "Vous pouvez créer une vue à partir d'ici avec autant de propriétés ou de filtres que vous le souhaitez." + } + } + }, + "filter": { + "title": "Aucune vue correspondante", + "description": "Aucune vue ne correspond aux critères de recherche. \n Créez une nouvelle vue à la place." + } + } + }, + + "project_page": { + "empty_state": { + "general": { + "title": "Écrivez une note, un document ou une base de connaissances complète. Laissez Galileo, l'assistant IA de Plane, vous aider à démarrer", + "description": "Les pages sont un espace pour capturer des idées dans Plane. Prenez des notes de réunion, formatez-les facilement, intégrez des problèmes, organisez-les en utilisant une bibliothèque de composants et gardez-les toutes dans le contexte de votre projet. Pour simplifier tout document, invoquez Galileo, l'IA de Plane, avec un raccourci ou en cliquant sur un bouton.", + "primary_button": { + "text": "Créez votre première page" + } + }, + "private": { + "title": "Aucune page privée pour le moment", + "description": "Conservez vos pensées privées ici. Lorsque vous êtes prêt à les partager, votre équipe n'est qu'à un clic.", + "primary_button": { + "text": "Créez votre première page" + } + }, + "public": { + "title": "Aucune page publique pour le moment", + "description": "Consultez ici les pages partagées avec tout le monde dans votre projet.", + "primary_button": { + "text": "Créez votre première page" + } + }, + "archived": { + "title": "Aucune page archivée pour le moment", + "description": "Archivez les pages qui ne sont pas sur votre radar. Accédez-y ici en cas de besoin." + } + } + }, + + "command_k": { + "empty_state": { + "search": { + "title": "Aucun résultat trouvé" + } + } + }, + + "issue_relation": { + "empty_state": { + "search": { + "title": "Aucun problème correspondant trouvé" + }, + "general": { + "title": "Aucun problème trouvé" + } + } + }, + + "issue_comment": { + "empty_state": { + "general": { + "title": "Pas encore de commentaires", + "description": "Les commentaires peuvent être utilisés comme un espace de discussion et de suivi pour les problèmes" + } + } + }, + + "notification": { + "empty_state": { + "detail": { + "title": "Sélectionnez pour voir les détails." + }, + "all": { + "title": "Aucune tâche assignée", + "description": "Les mises à jour des problèmes qui vous sont assignés peuvent être \n vues ici." + }, + "mentions": { + "title": "Aucune tâche assignée", + "description": "Les mises à jour des problèmes qui vous sont assignés peuvent être \n vues ici." + } + } + }, + + "active_cycle": { + "empty_state": { + "progress": { + "title": "Ajoutez des problèmes au cycle pour voir sa progression." + }, + "chart": { + "title": "Ajoutez des problèmes au cycle pour voir le graphique d'avancement." + }, + "priority_issue": { + "title": "Observez d'un coup d'œil les problèmes prioritaires traités dans le cycle." + }, + "assignee": { + "title": "Ajoutez des responsables aux problèmes pour voir la répartition du travail par responsable." + }, + "label": { + "title": "Ajoutez des étiquettes aux problèmes pour voir la répartition du travail par étiquette." + } + } + }, + + "disabled_project": { + "empty_state": { + "inbox": { + "title": "La collecte n'est pas activée pour le projet.", + "description": "La collecte vous aide à gérer les demandes entrantes pour votre projet et à les ajouter en tant que problèmes dans votre flux de travail. Activez la collecte dans les paramètres du projet pour gérer les demandes.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + }, + "cycle": { + "title": "Les cycles ne sont pas activés pour ce projet.", + "description": "Divisez le travail en segments limités dans le temps, travaillez à rebours depuis la date limite de votre projet pour fixer des dates et progressez concrètement en équipe. Activez la fonctionnalité des cycles pour votre projet afin de commencer à les utiliser.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + }, + "module": { + "title": "Les modules ne sont pas activés pour le projet.", + "description": "Les modules sont les éléments constitutifs de votre projet. Activez les modules dans les paramètres du projet pour commencer à les utiliser.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + }, + "page": { + "title": "Les pages ne sont pas activées pour le projet.", + "description": "Les pages sont les éléments constitutifs de votre projet. Activez les pages dans les paramètres du projet pour commencer à les utiliser.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + }, + "view": { + "title": "Les vues ne sont pas activées pour le projet.", + "description": "Les vues sont les éléments constitutifs de votre projet. Activez les vues dans les paramètres du projet pour commencer à les utiliser.", + "primary_button": { + "text": "Gérer les fonctionnalités" + } + } + } + }, + + "inbox": { + "label": "Boîte de réception", + "empty_state": { + "sidebar_open_tab": { + "title": "Aucun problème ouvert", + "description": "Trouvez ici les problèmes ouverts. Créez un nouveau problème." + }, + "sidebar_closed_tab": { + "title": "Aucun problème fermé", + "description": "Tous les problèmes, qu'ils soient acceptés ou refusés, se trouvent ici." + }, + "sidebar_filter": { + "title": "Aucun problème correspondant", + "description": "Aucun problème ne correspond au filtre appliqué dans l'entrée. Créez un nouveau problème." + }, + "detail": { + "title": "Sélectionnez un problème pour voir ses détails." + } + } + }, + + "workspace_draft_issues": { + "empty_state": { + "title": "Les problèmes à moitié rédigés, et bientôt, les commentaires apparaîtront ici.", + "description": "Pour essayer cela, commencez à rédiger un problème et laissez-le en suspens ou créez votre premier brouillon ci-dessous. 😉", + "primary_button": { + "text": "Créez votre premier brouillon" + } + } + }, + + "stickies": { + "empty_state": { + "general": { + "title": "Les stickies sont des notes rapides et des tâches que vous notez à la volée.", + "description": "Capturez vos pensées et vos idées facilement en créant des stickies accessibles à tout moment et de n'importe où.", + "primary_button": { + "text": "Ajouter un sticky" + } + }, + "search": { + "title": "Aucun sticky ne correspond à votre recherche.", + "description": "Essayez un autre terme ou faites-le nous savoir si vous êtes sûr que votre recherche est correcte.", + "primary_button": { + "text": "Ajouter un sticky" + } + } + } + }, + + "home_widgets": { + "empty_state": { + "general": { + "title": "C'est calme sans widgets, activez-les", + "description": "Il semble que tous vos widgets soient désactivés. Activez-les maintenant pour améliorer votre expérience !", + "primary_button": { + "text": "Gérer les widgets" + } + } + } + } } diff --git a/packages/i18n/src/locales/ja/translations.json b/packages/i18n/src/locales/ja/translations.json index fa2b244cc5e..d7c8e76322d 100644 --- a/packages/i18n/src/locales/ja/translations.json +++ b/packages/i18n/src/locales/ja/translations.json @@ -38,7 +38,6 @@ "deactivate_account_description": "アカウントを無効化すると、そのアカウント内のすべてのデータとリソースが完全に削除され、復元することはできません。", "profile_settings": "プロフィール設定", "your_account": "アカウント", - "profile": "プロフィール", "security": "セキュリティ", "activity": "アクティビティ", "appearance": "アピアンス", @@ -154,7 +153,6 @@ "stay_ahead_of_blockers_description": "プロジェクト間の課題を見つけ、他のビューからは明らかでないサイクル間の依存関係を確認します。", "analytics": "分析", "workspace_invites": "ワークスペースの招待", - "workspace_settings": "ワークスペース設定", "enter_god_mode": "ゴッドモードに入る", "workspace_logo": "ワークスペースロゴ", "new_issue": "新しい問題", @@ -316,5 +314,468 @@ "remove_parent_issue": "親問題を削除", "add_parent": "親問題を追加", "loading_members": "メンバーを読み込んでいます...", - "inbox": "受信箱" + "connections": "接続", + + "workspace_dashboard": { + "empty_state": { + "general": { + "title": "プロジェクト、アクティビティ、指標の概要", + "description": "Planeへようこそ!私たちはあなたを迎えることができて嬉しいです。最初のプロジェクトを作成してタスクを追跡し、このページが進捗を助けるスペースに変わります。管理者は、チームを前進させるための要素も見ることができます。", + "primary_button": { + "text": "最初のプロジェクトを作成", + "comic": { + "title": "すべてはPlaneでのプロジェクトから始まります", + "description": "プロジェクトは製品ロードマップ、マーケティングキャンペーン、新車の発売など、さまざまなものになります。" + } + } + } + } + }, + + "workspace_analytics": { + "empty_state": { + "general": { + "title": "進捗、作業負荷、割り当てを追跡します。傾向を見つけ、障害を取り除き、作業を加速させます", + "description": "需要に対するスコープ、見積もり、目標の進展を視覚化します。メンバーやチームごとのパフォーマンスを確認し、プロジェクトが期限内に進むことを確認します。", + "primary_button": { + "text": "最初のプロジェクトを開始", + "comic": { + "title": "分析はサイクル+モジュールでより効果的に機能します", + "description": "まず、タスクをサイクルにフレーム化し、可能であれば複数のサイクルにまたがるタスクをモジュールにグループ化します。左側のナビゲーションで確認してください。" + } + } + } + } + }, + + "workspace_projects": { + "empty_state": { + "general": { + "title": "アクティブなプロジェクトがありません", + "description": "各プロジェクトは目標志向の作業の親と見なされます。プロジェクトはタスク、サイクル、モジュールが存在する場所であり、同僚とともに目標を達成するのに役立ちます。新しいプロジェクトを作成するか、アーカイブされたプロジェクトをフィルタリングしてください。", + "primary_button": { + "text": "最初のプロジェクトを開始", + "comic": { + "title": "すべてはPlaneでのプロジェクトから始まります", + "description": "プロジェクトは製品ロードマップ、マーケティングキャンペーン、新車の発売など、さまざまなものになります。" + } + } + }, + "no_projects": { + "title": "プロジェクトがありません", + "description": "タスクを作成したり作業を管理するには、プロジェクトを作成するか、その一部である必要があります。", + "primary_button": { + "text": "最初のプロジェクトを開始", + "comic": { + "title": "すべてはPlaneでのプロジェクトから始まります", + "description": "プロジェクトは製品ロードマップ、マーケティングキャンペーン、新車の発売など、さまざまなものになります。" + } + } + }, + "filter": { + "title": "一致するプロジェクトがありません", + "description": "一致する条件のプロジェクトが見つかりませんでした。\n 代わりに新しいプロジェクトを作成してください。" + } + } + }, + + "workspace_issues": { + "empty_state": { + "all-issues": { + "title": "プロジェクトにタスクがありません", + "description": "最初のプロジェクトが完了しました!次に、追跡可能なチャンクに分割してタスクを作成します。始めましょう!", + "primary_button": { + "text": "新しいタスクを作成" + } + }, + "assigned": { + "title": "割り当てられたタスクがありません", + "description": "割り当てられたタスクをここで追跡できます。", + "primary_button": { + "text": "新しいタスクを作成" + } + }, + "created": { + "title": "作成されたタスクがありません", + "description": "作成したすべてのタスクはここにあります。直接ここで追跡してください。", + "primary_button": { + "text": "新しいタスクを作成" + } + }, + "subscribed": { + "title": "フォローしているタスクがありません", + "description": "興味のあるタスクをフォローして、すべてここで追跡してください。" + }, + "custom-view": { + "title": "タスクが見つかりません", + "description": "フィルタに一致するタスクがここに表示されます。すべてここで追跡してください。" + } + } + }, + + "workspace_settings": { + "label": "ワークスペース設定", + "empty_state": { + "api_tokens": { + "title": "APIトークンが作成されていません", + "description": "Plane APIは、外部システムでPlaneデータを統合するために使用できます。トークンを作成して始めましょう。" + }, + "webhooks": { + "title": "Webhookが追加されていません", + "description": "リアルタイム更新を受け取り、アクションを自動化するためにWebhookを作成します。" + }, + "exports": { + "title": "エクスポートはまだありません", + "description": "エクスポートするたびに、コピーがここにも参照用として利用可能になります。" + }, + "imports": { + "title": "インポートはまだありません", + "description": "過去のインポート履歴をすべてここで確認し、ダウンロードできます。" + } + } + }, + + "profile": { + "label": "プロフィール", + "empty_state": { + "activity": { + "title": "まだアクティビティがありません", + "description": "新しいタスクを作成することから始めましょう!詳細やプロパティを追加してください。Planeをさらに探索して、アクティビティを確認してください。" + }, + "assigned": { + "title": "割り当てられたタスクがありません", + "description": "割り当てられたタスクをここで追跡できます。" + }, + "created": { + "title": "作成されたタスクがありません", + "description": "作成したすべてのタスクはここにあります。直接ここで追跡してください。" + }, + "subscribed": { + "title": "フォローしているタスクがありません", + "description": "興味のあるタスクをフォローして、すべてここで追跡してください。" + } + } + }, + + "project_settings": { + "empty_state": { + "labels": { + "title": "ラベルがありません", + "description": "ラベルを作成して、プロジェクトのタスクを整理し、フィルタリングします。" + } + } + }, + + "project_cycles": { + "empty_state": { + "general": { + "title": "作業をサイクルにまとめ、フレーム化します。", + "description": "作業を定義された期間に分割し、期限から逆算して締め切りを設定し、チームで具体的な進捗を達成します。", + "primary_button": { + "text": "最初のサイクルを設定", + "comic": { + "title": "サイクルは繰り返し期間です。", + "description": "スプリント、イテレーション、または週次・隔週の作業追跡に使用するその他の用語がサイクルです。" + } + } + }, + "no_issues": { + "title": "サイクルに追加されたタスクがありません", + "description": "このサイクル内でタイムフレーム化し、配信したいタスクを追加または作成してください。", + "primary_button": { + "text": "新しいタスクを作成" + }, + "secondary_button": { + "text": "既存のタスクを追加" + } + }, + "completed_no_issues": { + "title": "サイクルに課題がありません", + "description": "サイクルに課題がありません。課題は転送されるか非表示になっています。非表示の課題がある場合は、表示プロパティを更新して確認してください。" + }, + "active": { + "title": "アクティブなサイクルがありません", + "description": "アクティブなサイクルは、今日の日付を含む範囲がある期間を指します。ここでアクティブなサイクルの進捗と詳細を確認してください。" + }, + "archived": { + "title": "まだアーカイブされたサイクルはありません", + "description": "プロジェクトを整理するには、完了したサイクルをアーカイブしてください。アーカイブされたサイクルはここで見つけることができます。" + } + } + }, + + "project_issues": { + "empty_state": { + "no_issues": { + "title": "課題を作成し、誰かに、または自分に割り当てましょう", + "description": "課題を仕事、タスク、行動、またはJTBD(やるべき仕事)と考えてください。それが私たちの好む方法です。課題とそのサブ課題は通常、チームメンバーに割り当てられる時間ベースのアクションです。チームは課題を作成し、割り当て、完了させることで、プロジェクトの目標に向かって進みます。", + "primary_button": { + "text": "最初の課題を作成", + "comic": { + "title": "課題はPlaneの構成要素です。", + "description": "Plane UIの再設計、会社のリブランド、新しい燃料噴射システムの開始は、サブ課題がある可能性のある課題の例です。" + } + } + }, + "no_archived_issues": { + "title": "まだアーカイブされた課題はありません", + "description": "手動または自動化を使用して、完了またはキャンセルされた課題をアーカイブできます。アーカイブされた課題はここで見つけることができます。", + "primary_button": { + "text": "自動化を設定" + } + }, + "issues_empty_filter": { + "title": "適用されたフィルターに一致する課題は見つかりませんでした", + "secondary_button": { + "text": "すべてのフィルターをクリア" + } + } + } + }, + + "project_module": { + "empty_state": { + "no_issues": { + "title": "モジュールに課題がありません", + "description": "このモジュールの一部として達成したい課題を作成または追加してください", + "primary_button": { + "text": "新しい課題を作成" + }, + "secondary_button": { + "text": "既存の課題を追加" + } + }, + "general": { + "title": "プロジェクトのマイルストーンをモジュールにマッピングし、集計された作業を簡単に追跡します。", + "description": "論理的で階層的な親に属する課題のグループがモジュールを形成します。それをプロジェクトのマイルストーンで作業を追跡する方法と考えてください。モジュールには独自の期間と締切があり、分析を通じてマイルストーンへの進捗状況を確認できます。", + "primary_button": { + "text": "最初のモジュールを構築", + "comic": { + "title": "モジュールは階層によって作業をグループ化します。", + "description": "カートモジュール、シャーシモジュール、倉庫モジュールは、このグループ化の良い例です。" + } + } + }, + "archived": { + "title": "まだアーカイブされたモジュールはありません", + "description": "プロジェクトを整理するために、完了またはキャンセルされたモジュールをアーカイブします。アーカイブされたモジュールはここで見つけることができます。" + } + } + }, + + "project_views": { + "empty_state": { + "general": { + "title": "プロジェクト用にフィルタされたビューを保存します。必要なだけ作成してください", + "description": "ビューは、頻繁に使用するか、簡単にアクセスしたい保存されたフィルターのセットです。プロジェクト内のすべての同僚が他の人のビューを確認し、自分のニーズに最適なものを選ぶことができます。", + "primary_button": { + "text": "最初のビューを作成", + "comic": { + "title": "ビューは課題プロパティの上に機能します。", + "description": "ここから必要に応じてプロパティをフィルターとして使用してビューを作成できます。" + } + } + }, + "filter": { + "title": "一致するビューがありません", + "description": "検索条件に一致するビューはありません。 \n 代わりに新しいビューを作成してください。" + } + } + }, + + "project_page": { + "empty_state": { + "general": { + "title": "メモ、ドキュメント、または完全なナレッジベースを書きましょう。PlaneのAIアシスタントGalileoがスタートをサポートします", + "description": "ページはPlaneでアイデアをまとめるスペースです。会議のメモを取る、簡単にフォーマットする、課題を埋め込む、コンポーネントライブラリを使用してレイアウトするなど、すべてをプロジェクトのコンテキスト内に保ちます。どんなドキュメントでも簡単にするために、ショートカットやボタンのクリックでPlaneのAIであるGalileoを呼び出してください。", + "primary_button": { + "text": "最初のページを作成" + } + }, + "private": { + "title": "まだプライベートページはありません", + "description": "プライベートな考えをここに保存します。共有する準備ができたら、チームはすぐそばにいます。", + "primary_button": { + "text": "最初のページを作成" + } + }, + "public": { + "title": "まだ公開ページはありません", + "description": "プロジェクト内のすべての人と共有されているページをここで確認してください。", + "primary_button": { + "text": "最初のページを作成" + } + }, + "archived": { + "title": "まだアーカイブされたページはありません", + "description": "レーダーに載っていないページをアーカイブします。必要なときにここからアクセスしてください。" + } + } + }, + + "command_k": { + "empty_state": { + "search": { + "title": "結果が見つかりません" + } + } + }, + + "issue_relation": { + "empty_state": { + "search": { + "title": "一致する課題が見つかりませんでした" + }, + "general": { + "title": "課題が見つかりません" + } + } + }, + + "issue_comment": { + "empty_state": { + "general": { + "title": "まだコメントはありません", + "description": "コメントは課題の議論やフォローアップスペースとして使用できます" + } + } + }, + + "notification": { + "empty_state": { + "detail": { + "title": "詳細を表示するには選択してください。" + }, + "all": { + "title": "割り当てられた問題はありません", + "description": "あなたに割り当てられた問題の更新情報はここで確認できます。" + }, + "mentions": { + "title": "割り当てられた問題はありません", + "description": "あなたに割り当てられた問題の更新情報はここで確認できます。" + } + } + }, + + "active_cycle": { + "empty_state": { + "progress": { + "title": "サイクルに課題を追加して進捗を確認してください。" + }, + "chart": { + "title": "サイクルに課題を追加してバーンダウンチャートを確認してください。" + }, + "priority_issue": { + "title": "サイクル内で処理された高優先度の課題を一目で把握できます。" + }, + "assignee": { + "title": "課題に担当者を追加して、担当者ごとの作業の内訳を確認してください。" + }, + "label": { + "title": "課題にラベルを追加して、ラベルごとの作業の内訳を確認してください。" + } + } + }, + + "disabled_project": { + "empty_state": { + "inbox": { + "title": "プロジェクトにインテークが有効化されていません。", + "description": "インテークは、プロジェクトへのリクエストを管理し、それらをワークフローの課題として追加するのに役立ちます。プロジェクト設定からインテークを有効化してリクエストを管理してください。", + "primary_button": { + "text": "機能を管理" + } + }, + "cycle": { + "title": "このプロジェクトでサイクルが有効化されていません。", + "description": "作業をタイムボックス化されたチャンクに分割し、プロジェクトの期限から逆算して日程を設定し、チームとして具体的な進捗を実現します。プロジェクトでサイクル機能を有効化して利用を開始してください。", + "primary_button": { + "text": "機能を管理" + } + }, + "module": { + "title": "プロジェクトにモジュールが有効化されていません。", + "description": "モジュールはプロジェクトの構成要素です。プロジェクト設定からモジュールを有効化して利用を開始してください。", + "primary_button": { + "text": "機能を管理" + } + }, + "page": { + "title": "プロジェクトにページが有効化されていません。", + "description": "ページはプロジェクトの構成要素です。プロジェクト設定からページを有効化して利用を開始してください。", + "primary_button": { + "text": "機能を管理" + } + }, + "view": { + "title": "プロジェクトにビューが有効化されていません。", + "description": "ビューはプロジェクトの構成要素です。プロジェクト設定からビューを有効化して利用を開始してください。", + "primary_button": { + "text": "機能を管理" + } + } + } + }, + + "inbox": { + "label": "受信トレイ", + "empty_state": { + "sidebar_open_tab": { + "title": "未解決の課題はありません", + "description": "ここに未解決の課題が表示されます。新しい課題を作成してください。" + }, + "sidebar_closed_tab": { + "title": "完了した課題はありません", + "description": "受け入れ済みまたは却下済みのすべての課題がここに表示されます。" + }, + "sidebar_filter": { + "title": "一致する課題はありません", + "description": "入力で適用されたフィルターに一致する課題はありません。新しい課題を作成してください。" + }, + "detail": { + "title": "詳細を表示するには課題を選択してください。" + } + } + }, + + "workspace_draft_issues": { + "empty_state": { + "title": "書きかけの課題や、もうすぐコメントもここに表示されます。", + "description": "これを試すには、課題の作成を始めて途中で中断するか、下に最初の下書きを作成してください。😉", + "primary_button": { + "text": "最初の下書きを作成" + } + } + }, + + "stickies": { + "empty_state": { + "general": { + "title": "スティッキーは、その場で素早く取るメモやタスクです。", + "description": "いつでもどこでもアクセスできるスティッキーを作成して、思いついたアイデアを簡単にキャプチャしましょう。", + "primary_button": { + "text": "スティッキーを追加" + } + }, + "search": { + "title": "一致するスティッキーはありません。", + "description": "別の用語を試すか、検索が正しい場合はお知らせください。", + "primary_button": { + "text": "スティッキーを追加" + } + } + } + }, + + "home_widgets": { + "empty_state": { + "general": { + "title": "ウィジェットがないと静かですね、オンにしましょう", + "description": "すべてのウィジェットがオフになっているようです。 今すぐ有効にして、体験を向上させましょう!", + "primary_button": { + "text": "ウィジェットを管理" + } + } + } + } } diff --git a/space/app/layout.tsx b/space/app/layout.tsx index e457ae5d1d1..488fb551b42 100644 --- a/space/app/layout.tsx +++ b/space/app/layout.tsx @@ -32,7 +32,9 @@ export default function RootLayout({ children }: { children: React.ReactNode }) - {children} + + <>{children} + ); diff --git a/space/core/components/issues/issue-layouts/kanban/default.tsx b/space/core/components/issues/issue-layouts/kanban/default.tsx index 50f7b0f7138..6ad7b2e0f17 100644 --- a/space/core/components/issues/issue-layouts/kanban/default.tsx +++ b/space/core/components/issues/issue-layouts/kanban/default.tsx @@ -94,7 +94,7 @@ export const KanBan: React.FC = observer((props) => {
diff --git a/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx b/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx index 902dff670d0..bc61c54af6e 100644 --- a/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/space/core/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -134,7 +134,7 @@ const SubGroupSwimlaneHeader: React.FC = observer( return (
- +
); })} @@ -262,7 +262,7 @@ const SubGroup: React.FC = observer((props) => {
{ const searchParams = useSearchParams(); const analytics_tab = searchParams.get("analytics_tab"); + // plane imports + const { t } = useTranslation(); // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); const { workspaceProjectIds, loader } = useProject(); const { currentWorkspace } = useWorkspace(); + const { allowPermissions } = useUserPermissions(); + // helper hooks + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/analytics" }); // derived values const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Analytics` : undefined; + // permissions + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + // TODO: refactor loader implementation return ( <> @@ -67,12 +80,22 @@ const AnalyticsPage = observer(() => {
) : ( - { - setTrackElement("Analytics empty state"); - toggleCreateProjectModal(true); - }} + { + setTrackElement("Analytics empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } /> )} diff --git a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx index 4ea0c8e4258..3074acbf8ce 100644 --- a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx @@ -4,21 +4,24 @@ import { useCallback, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; +// plane imports +import { useTranslation } from "@plane/i18n"; // components import { LogoSpinner } from "@/components/common"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; import { InboxContentRoot } from "@/components/inbox"; import { IssuePeekOverview } from "@/components/issues"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification"; // hooks import { useIssueDetail, useUserPermissions, useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; const WorkspaceDashboardPage = observer(() => { const { workspaceSlug } = useParams(); + // plane hooks + const { t } = useTranslation(); // hooks const { currentWorkspace } = useWorkspace(); const { @@ -34,6 +37,7 @@ const WorkspaceDashboardPage = observer(() => { const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Inbox` : undefined; const { workspace_slug, project_id, issue_id, is_inbox_issue } = notificationLiteByNotificationId(currentSelectedNotificationId); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" }); // fetching workspace issue properties useWorkspaceIssueProperties(workspaceSlug); @@ -82,7 +86,7 @@ const WorkspaceDashboardPage = observer(() => {
{!currentSelectedNotificationId ? (
- +
) : ( <> diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx index 5b1793d5d1f..b72b46dbeaf 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/cycles/(list)/page.tsx @@ -3,20 +3,22 @@ import { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// types +// plane imports +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TCycleFilters } from "@plane/types"; // components import { Header, EHeaderVariant } from "@plane/ui"; import { PageHead } from "@/components/core"; import { CyclesView, CycleCreateUpdateModal, CycleAppliedFiltersList } from "@/components/cycles"; -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { CycleModuleListLayout } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useEventTracker, useCycle, useProject, useCycleFilter } from "@/hooks/store"; +import { useEventTracker, useCycle, useProject, useCycleFilter, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const ProjectCyclesPage = observer(() => { // states @@ -26,13 +28,23 @@ const ProjectCyclesPage = observer(() => { const { currentProjectCycleIds, loader } = useCycle(); const { getProjectById, currentProjectDetails } = useProject(); // router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // cycle filters hook const { clearAllFilters, currentProjectFilters, updateFilters } = useCycleFilter(); + const { allowPermissions } = useUserPermissions(); // derived values const totalCycles = currentProjectCycleIds?.length ?? 0; const project = projectId ? getProjectById(projectId?.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Cycles` : undefined; + const hasAdminLevelPermission = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const hasMemberLevelPermission = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/cycles" }); const handleRemoveFilter = (key: keyof TCycleFilters, value: string | null) => { if (!projectId) return; @@ -50,9 +62,17 @@ const ProjectCyclesPage = observer(() => { if (currentProjectDetails?.cycle_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !hasAdminLevelPermission, + }} />
); @@ -71,12 +91,22 @@ const ProjectCyclesPage = observer(() => { /> {totalCycles === 0 ? (
- { - setTrackElement("Cycle empty state"); - setCreateModal(true); - }} + { + setTrackElement("Cycle empty state"); + setCreateModal(true); + }} + disabled={!hasMemberLevelPermission} + /> + } />
) : ( diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/page.tsx index 36aa37e302c..92d01aa25a9 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/inbox/page.tsx @@ -2,35 +2,50 @@ import { observer } from "mobx-react"; // components import { useParams, useSearchParams } from "next/navigation"; +import { EUserProjectRoles } from "@plane/constants/src/user"; +import { useTranslation } from "@plane/i18n"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { InboxIssueRoot } from "@/components/inbox"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; // hooks -import { useProject } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const ProjectInboxPage = observer(() => { /// router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); - const searchParams = useSearchParams(); - const navigationTab = searchParams.get("currentTab"); const inboxIssueId = searchParams.get("inboxIssueId"); - + // plane hooks + const { t } = useTranslation(); // hooks const { currentProjectDetails } = useProject(); + const { allowPermissions } = useUserPermissions(); + // derived values + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/intake" }); // No access to inbox if (currentProjectDetails?.inbox_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !canPerformEmptyStateActions, + }} />
); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx index 9417016e385..572cb3862f7 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/modules/(list)/page.tsx @@ -4,27 +4,36 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // types +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TModuleFilters } from "@plane/types"; // components import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { ModuleAppliedFiltersList, ModulesListView } from "@/components/modules"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useModuleFilter, useProject } from "@/hooks/store"; +import { useModuleFilter, useProject, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const ProjectModulesPage = observer(() => { + // router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store const { getProjectById, currentProjectDetails } = useProject(); const { currentProjectFilters, currentProjectDisplayFilters, clearAllFilters, updateFilters, updateDisplayFilters } = useModuleFilter(); + const { allowPermissions } = useUserPermissions(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Modules` : undefined; + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/modules" }); const handleRemoveFilter = useCallback( (key: keyof TModuleFilters, value: string | null) => { @@ -45,9 +54,17 @@ const ProjectModulesPage = observer(() => { if (currentProjectDetails?.module_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !canPerformEmptyStateActions, + }} />
); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx index 93f37ea83b0..547584f51a1 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/pages/(list)/page.tsx @@ -2,27 +2,35 @@ import { observer } from "mobx-react"; import { useParams, useSearchParams } from "next/navigation"; -// types +// plane imports +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TPageNavigationTabs } from "@plane/types"; // components import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { PagesListRoot, PagesListView } from "@/components/pages"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useProject } from "@/hooks/store"; +import { useProject, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const ProjectPagesPage = observer(() => { // router + const router = useAppRouter(); const searchParams = useSearchParams(); const type = searchParams.get("type"); const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { getProjectById, currentProjectDetails } = useProject(); + const { allowPermissions } = useUserPermissions(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Pages` : undefined; + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/pages" }); const currentPageType = (): TPageNavigationTabs => { const pageType = type?.toString(); @@ -37,9 +45,17 @@ const ProjectPagesPage = observer(() => { if (currentProjectDetails?.page_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !canPerformEmptyStateActions, + }} />
); diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx index 387f6e5b423..73e7e0d85e4 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/views/(list)/page.tsx @@ -4,29 +4,38 @@ import { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TViewFilterProps } from "@plane/types"; import { Header, EHeaderVariant } from "@plane/ui"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { ProjectViewsList } from "@/components/views"; import { ViewAppliedFiltersList } from "@/components/views/applied-filters"; -import { EmptyStateType } from "@/constants/empty-state"; // constants import { EViewAccess } from "@/constants/views"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks -import { useProject, useProjectView } from "@/hooks/store"; +import { useProject, useProjectView, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const ProjectViewsPage = observer(() => { // router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store const { getProjectById, currentProjectDetails } = useProject(); const { filters, updateFilters, clearAllFilters } = useProjectView(); + const { allowPermissions } = useUserPermissions(); // derived values const project = projectId ? getProjectById(projectId.toString()) : undefined; const pageTitle = project?.name ? `${project?.name} - Views` : undefined; + const canPerformEmptyStateActions = allowPermissions([EUserProjectRoles.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/disabled-feature/views" }); const handleRemoveFilter = useCallback( (key: keyof TViewFilterProps, value: string | EViewAccess | null) => { @@ -53,9 +62,17 @@ const ProjectViewsPage = observer(() => { if (currentProjectDetails?.issue_views_view === false) return (
- { + router.push(`/${workspaceSlug}/projects/${projectId}/settings/features`); + }, + disabled: !canPerformEmptyStateActions, + }} />
); diff --git a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx index fd2ed9669b2..03bb1fa6cbe 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/api-tokens/page.tsx @@ -4,19 +4,19 @@ import React, { useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // component import { ApiTokenListItem, CreateApiTokenModal } from "@/components/api-token"; import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { APITokenSettingsLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; import { API_TOKENS_LIST } from "@/constants/fetch-keys"; // store hooks import { useUserPermissions, useWorkspace } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services import { APITokenService } from "@/services/api_token.service"; @@ -28,11 +28,14 @@ const ApiTokensPage = observer(() => { const [isCreateTokenModalOpen, setIsCreateTokenModalOpen] = useState(false); // router const { workspaceSlug } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { currentWorkspace } = useWorkspace(); const { workspaceUserInfo, allowPermissions } = useUserPermissions(); // derived values const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/api-tokens" }); const { data: tokens } = useSWR( workspaceSlug && canPerformWorkspaceAdminActions ? API_TOKENS_LIST(workspaceSlug.toString()) : null, @@ -78,7 +81,11 @@ const ApiTokensPage = observer(() => {
- +
)} diff --git a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx index 86c922f07fb..412c29f5dd2 100644 --- a/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/settings/webhooks/page.tsx @@ -4,18 +4,18 @@ import React, { useEffect, useState } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // components import { NotAuthorizedView } from "@/components/auth-screens"; import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { WebhookSettingsLoader } from "@/components/ui"; import { WebhooksList, CreateWebhookModal } from "@/components/web-hooks"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks import { useUserPermissions, useWebhook, useWorkspace } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; const WebhooksListPage = observer(() => { @@ -23,13 +23,15 @@ const WebhooksListPage = observer(() => { const [showCreateWebhookModal, setShowCreateWebhookModal] = useState(false); // router const { workspaceSlug } = useParams(); + // plane hooks + const { t } = useTranslation(); // mobx store const { workspaceUserInfo, allowPermissions } = useUserPermissions(); - const { fetchWebhooks, webhooks, clearSecretKey, webhookSecretKey, createWebhook } = useWebhook(); const { currentWorkspace } = useWorkspace(); - + // derived values const canPerformWorkspaceAdminActions = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.WORKSPACE); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/webhooks" }); useSWR( workspaceSlug && canPerformWorkspaceAdminActions ? `WEBHOOKS_LIST_${workspaceSlug}` : null, @@ -81,7 +83,11 @@ const WebhooksListPage = observer(() => {
- +
)} diff --git a/web/app/profile/activity/page.tsx b/web/app/profile/activity/page.tsx index a2a8cad851f..ca6c9511d06 100644 --- a/web/app/profile/activity/page.tsx +++ b/web/app/profile/activity/page.tsx @@ -7,24 +7,27 @@ import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // components import { PageHead } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { ProfileActivityListPage, ProfileSettingContentHeader, ProfileSettingContentWrapper, } from "@/components/profile"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +// hooks +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; const PER_PAGE = 100; const ProfileActivityPage = observer(() => { - const { t } = useTranslation(); // states const [pageCount, setPageCount] = useState(1); const [totalPages, setTotalPages] = useState(0); const [resultsCount, setResultsCount] = useState(0); const [isEmpty, setIsEmpty] = useState(false); + // plane hooks + const { t } = useTranslation(); + // derived values + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/profile/activity" }); const updateTotalPages = (count: number) => setTotalPages(count); @@ -50,7 +53,13 @@ const ProfileActivityPage = observer(() => { const isLoadMoreVisible = pageCount < totalPages && resultsCount !== 0; if (isEmpty) { - return ; + return ( + + ); } return ( diff --git a/web/app/profile/notifications/page.tsx b/web/app/profile/notifications/page.tsx index cbdcd147d73..5e154fdffc1 100644 --- a/web/app/profile/notifications/page.tsx +++ b/web/app/profile/notifications/page.tsx @@ -25,7 +25,7 @@ export default function ProfileNotificationPage() { return ( <> - + { return ( <> - + diff --git a/web/ce/components/cycles/active-cycle/root.tsx b/web/ce/components/cycles/active-cycle/root.tsx index 5ebddc63f23..a13b7f64cfa 100644 --- a/web/ce/components/cycles/active-cycle/root.tsx +++ b/web/ce/components/cycles/active-cycle/root.tsx @@ -3,7 +3,8 @@ import { useMemo } from "react"; import { observer } from "mobx-react"; import { Disclosure } from "@headlessui/react"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Row } from "@plane/ui"; // components import { @@ -14,10 +15,10 @@ import { CyclesListItem, } from "@/components/cycles"; import useCyclesDetails from "@/components/cycles/active-cycle/use-cycles-details"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; +// hooks import { useCycle } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; interface IActiveCycleDetails { @@ -29,9 +30,13 @@ interface IActiveCycleDetails { export const ActiveCycleRoot: React.FC = observer((props) => { const { workspaceSlug, projectId, cycleId: propsCycleId, showHeader = true } = props; + // plane hooks + const { t } = useTranslation(); + // store hooks const { currentProjectActiveCycleId } = useCycle(); // derived values const cycleId = propsCycleId ?? currentProjectActiveCycleId; + const activeCycleResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/cycle/active" }); // fetch cycle details const { handleFiltersUpdate, @@ -43,7 +48,11 @@ export const ActiveCycleRoot: React.FC = observer((props) = () => ( <> {!cycleId || !activeCycle ? ( - + ) : (
{cycleId && ( diff --git a/web/core/components/command-palette/command-modal.tsx b/web/core/components/command-palette/command-modal.tsx index b80b0dfe145..8422288cf51 100644 --- a/web/core/components/command-palette/command-modal.tsx +++ b/web/core/components/command-palette/command-modal.tsx @@ -2,14 +2,15 @@ import React, { useEffect, useState } from "react"; import { Command } from "cmdk"; +import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; import { FolderPlus, Search, Settings } from "lucide-react"; import { Dialog, Transition } from "@headlessui/react"; -// types +// plane imports +import { useTranslation } from "@plane/i18n"; import { IWorkspaceSearchResults } from "@plane/types"; -// ui import { LayersIcon, Loader, ToggleSwitch, Tooltip } from "@plane/ui"; // components import { @@ -23,9 +24,7 @@ import { CommandPaletteThemeActions, CommandPaletteWorkspaceSettingsActions, } from "@/components/command-palette"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // fetch-keys import { ISSUE_DETAILS } from "@/constants/fetch-keys"; // helpers @@ -36,21 +35,20 @@ import { useAppRouter } from "@/hooks/use-app-router"; import useDebounce from "@/hooks/use-debounce"; import { usePlatformOS } from "@/hooks/use-platform-os"; // plane web components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { IssueIdentifier } from "@/plane-web/components/issues"; // plane web services import { WorkspaceService } from "@/plane-web/services"; // services import { IssueService } from "@/services/issue"; -import { EUserPermissions, EUserPermissionsLevel } from "ee/constants/user-permissions"; const workspaceService = new WorkspaceService(); const issueService = new IssueService(); export const CommandModal: React.FC = observer(() => { - // hooks - const { workspaceProjectIds } = useProject(); - const { isMobile } = usePlatformOS(); - const { canPerformAnyCreateAction } = useUser(); + // router + const router = useAppRouter(); + const { workspaceSlug, projectId, issueId } = useParams(); // states const [placeholder, setPlaceholder] = useState("Type a command or search..."); const [resultsCount, setResultsCount] = useState(0); @@ -70,26 +68,25 @@ export const CommandModal: React.FC = observer(() => { }); const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false); const [pages, setPages] = useState([]); + // plane hooks + const { t } = useTranslation(); + // hooks + const { workspaceProjectIds } = useProject(); + const { isMobile } = usePlatformOS(); + const { canPerformAnyCreateAction } = useUser(); const { isCommandPaletteOpen, toggleCommandPaletteModal, toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette(); const { allowPermissions } = useUserPermissions(); const { setTrackElement } = useEventTracker(); - - // router - const router = useAppRouter(); - // router params - const { workspaceSlug, projectId, issueId } = useParams(); - + // derived values const page = pages[pages.length - 1]; - const debouncedSearchTerm = useDebounce(searchTerm, 500); - const { baseTabIndex } = getTabIndex(undefined, isMobile); - const canPerformWorkspaceActions = allowPermissions( [EUserPermissions.ADMIN, EUserPermissions.MEMBER], EUserPermissionsLevel.WORKSPACE ); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); // TODO: update this to mobx store const { data: issueDetails } = useSWR( @@ -268,7 +265,7 @@ export const CommandModal: React.FC = observer(() => { {!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
- +
)} diff --git a/web/core/components/core/modals/bulk-delete-issues-modal.tsx b/web/core/components/core/modals/bulk-delete-issues-modal.tsx index aec70262ab1..73dd37a231a 100644 --- a/web/core/components/core/modals/bulk-delete-issues-modal.tsx +++ b/web/core/components/core/modals/bulk-delete-issues-modal.tsx @@ -6,19 +6,18 @@ import { useParams } from "next/navigation"; import { SubmitHandler, useForm } from "react-hook-form"; import { Search } from "lucide-react"; import { Combobox, Dialog, Transition } from "@headlessui/react"; +// plane imports import { EIssuesStoreType } from "@plane/constants"; -// types +import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse, IUser } from "@plane/types"; -// ui import { Button, Loader, TOAST_TYPE, setToast } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useIssues } from "@/hooks/store"; import useDebounce from "@/hooks/use-debounce"; // services +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { ProjectService } from "@/services/project"; // local components import { BulkDeleteIssuesModalItem } from "./bulk-delete-issues-modal-item"; @@ -39,16 +38,19 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { const { isOpen, onClose } = props; // router params const { workspaceSlug, projectId } = useParams(); - // hooks - const { - issues: { removeBulkIssues }, - } = useIssues(EIssuesStoreType.PROJECT); // states const [query, setQuery] = useState(""); const [issues, setIssues] = useState([]); const [isSearching, setIsSearching] = useState(false); - + // hooks + const { + issues: { removeBulkIssues }, + } = useIssues(EIssuesStoreType.PROJECT); + const { t } = useTranslation(); + // derived values const debouncedSearchTerm: string = useDebounce(query, 500); + const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); + const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" }); useEffect(() => { if (!isOpen || !workspaceSlug || !projectId) return; @@ -131,12 +133,11 @@ export const BulkDeleteIssuesModal: React.FC = observer((props) => { ) : (
- + {query === "" ? ( + + ) : ( + + )}
); diff --git a/web/core/components/core/modals/issue-search-modal-empty-state.tsx b/web/core/components/core/modals/issue-search-modal-empty-state.tsx index 578d39a60fc..20646efb182 100644 --- a/web/core/components/core/modals/issue-search-modal-empty-state.tsx +++ b/web/core/components/core/modals/issue-search-modal-empty-state.tsx @@ -1,10 +1,10 @@ import React from "react"; -// components +// plane imports +import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse } from "@plane/types"; -import { EmptyState } from "@/components/empty-state"; -// types -import { EmptyStateType } from "@/constants/empty-state"; -// constants +// components +import { SimpleEmptyState } from "@/components/empty-state"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; interface EmptyStateProps { issues: ISearchIssueResponse[]; @@ -19,18 +19,28 @@ export const IssueSearchModalEmptyState: React.FC = ({ debouncedSearchTerm, isSearching, }) => { - const renderEmptyState = (type: EmptyStateType) => ( -
- -
- ); + // plane hooks + const { t } = useTranslation(); + // derived values + const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); + const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" }); - const emptyState = - issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching - ? renderEmptyState(EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE) - : issues.length === 0 - ? renderEmptyState(EmptyStateType.ISSUE_RELATION_EMPTY_STATE) - : null; + const EmptyStateContainer = ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ); - return emptyState; + if (issues.length === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && !isSearching) { + return ( + + + + ); + } else if (issues.length === 0) { + return ( + + + + ); + } + return null; }; diff --git a/web/core/components/cycles/active-cycle/cycle-stats.tsx b/web/core/components/cycles/active-cycle/cycle-stats.tsx index 44d7b595f48..b9cf267a62f 100644 --- a/web/core/components/cycles/active-cycle/cycle-stats.tsx +++ b/web/core/components/cycles/active-cycle/cycle-stats.tsx @@ -6,17 +6,16 @@ import { observer } from "mobx-react"; import { CalendarCheck } from "lucide-react"; // headless ui import { Tab } from "@headlessui/react"; -// types +// plane imports import { EIssuesStoreType } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ICycle, IIssueFilterOptions } from "@plane/types"; // ui import { Tooltip, Loader, PriorityIcon, Avatar } from "@plane/ui"; // components import { SingleProgressStats } from "@/components/core"; import { StateDropdown } from "@/components/dropdowns"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; import { renderFormattedDate, renderFormattedDateWithoutYear } from "@/helpers/date-time.helper"; @@ -26,6 +25,7 @@ import { useIssueDetail, useIssues } from "@/hooks/store"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; import useLocalStorage from "@/hooks/use-local-storage"; // plane web components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { IssueIdentifier } from "@/plane-web/components/issues"; // store import { ActiveCycleIssueDetails } from "@/store/issue/cycle"; @@ -41,11 +41,18 @@ export type ActiveCycleStatsProps = { export const ActiveCycleStats: FC = observer((props) => { const { workspaceSlug, projectId, cycle, cycleId, handleFiltersUpdate, cycleIssueDetails } = props; - + // local storage const { storedValue: tab, setValue: setTab } = useLocalStorage("activeCycleTab", "Assignees"); - + // refs const issuesContainerRef = useRef(null); + // states const [issuesLoaderElement, setIssueLoaderElement] = useState(null); + // plane hooks + const { t } = useTranslation(); + // derived values + const priorityResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/priority" }); + const assigneesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/assignee" }); + const labelsResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/label" }); const currentValue = (tab: string | null) => { switch (tab) { @@ -231,10 +238,9 @@ export const ActiveCycleStats: FC = observer((props) => { ) : (
-
) @@ -293,10 +299,9 @@ export const ActiveCycleStats: FC = observer((props) => { }) ) : (
-
) @@ -336,7 +341,7 @@ export const ActiveCycleStats: FC = observer((props) => { )) ) : (
- +
) ) : ( diff --git a/web/core/components/cycles/active-cycle/productivity.tsx b/web/core/components/cycles/active-cycle/productivity.tsx index 74957af03c7..859c3015435 100644 --- a/web/core/components/cycles/active-cycle/productivity.tsx +++ b/web/core/components/cycles/active-cycle/productivity.tsx @@ -1,16 +1,17 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react"; import Link from "next/link"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { ICycle, TCycleEstimateType } from "@plane/types"; import { Loader } from "@plane/ui"; // components import ProgressChart from "@/components/core/sidebar/progress-chart"; -import { EmptyState } from "@/components/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // constants -import { EmptyStateType } from "@/constants/empty-state"; -import { useCycle, useProjectEstimates } from "@/hooks/store"; +import { useCycle } from "@/hooks/store"; // plane web constants -import { EEstimateSystem } from "@/plane-web/constants/estimates"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { EstimateTypeDropdown } from "../dropdowns/estimate-type-dropdown"; export type ActiveCycleProductivityProps = { @@ -21,11 +22,13 @@ export type ActiveCycleProductivityProps = { export const ActiveCycleProductivity: FC = observer((props) => { const { workspaceSlug, projectId, cycle } = props; + // plane hooks + const { t } = useTranslation(); // hooks const { getEstimateTypeByCycleId, setEstimateType } = useCycle(); - // derived values const estimateType: TCycleEstimateType = (cycle && getEstimateTypeByCycleId(cycle.id)) || "issues"; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/chart" }); const onChange = async (value: TCycleEstimateType) => { if (!workspaceSlug || !projectId || !cycle || !cycle.id) return; @@ -95,7 +98,7 @@ export const ActiveCycleProductivity: FC = observe ) : ( <>
- +
)} diff --git a/web/core/components/cycles/active-cycle/progress.tsx b/web/core/components/cycles/active-cycle/progress.tsx index 0dffff5c757..0f2c5a419aa 100644 --- a/web/core/components/cycles/active-cycle/progress.tsx +++ b/web/core/components/cycles/active-cycle/progress.tsx @@ -4,14 +4,14 @@ import { FC } from "react"; import { observer } from "mobx-react"; // plane package imports import { PROGRESS_STATE_GROUPS_DETAILS } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ICycle, IIssueFilterOptions } from "@plane/types"; import { LinearProgressIndicator, Loader } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useProjectState } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export type ActiveCycleProgressProps = { cycle: ICycle | null; @@ -22,9 +22,10 @@ export type ActiveCycleProgressProps = { export const ActiveCycleProgress: FC = observer((props) => { const { handleFiltersUpdate, cycle } = props; + // plane hooks + const { t } = useTranslation(); // store hooks const { groupedProjectStates } = useProjectState(); - // derived values const progressIndicatorData = PROGRESS_STATE_GROUPS_DETAILS.map((group, index) => ({ id: index, @@ -40,6 +41,7 @@ export const ActiveCycleProgress: FC = observer((props backlog: cycle?.backlog_issues, } : {}; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/active-cycle/progress" }); return cycle && cycle.hasOwnProperty("started_issues") ? (
@@ -101,7 +103,7 @@ export const ActiveCycleProgress: FC = observer((props
) : (
- +
)}
diff --git a/web/core/components/cycles/archived-cycles/root.tsx b/web/core/components/cycles/archived-cycles/root.tsx index 06f61b66f51..165b5e4aa34 100644 --- a/web/core/components/cycles/archived-cycles/root.tsx +++ b/web/core/components/cycles/archived-cycles/root.tsx @@ -2,28 +2,31 @@ import React from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -// types +// plane imports +import { useTranslation } from "@plane/i18n"; import { TCycleFilters } from "@plane/types"; // components import { ArchivedCyclesView, CycleAppliedFiltersList } from "@/components/cycles"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { CycleModuleListLayout } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useCycle, useCycleFilter } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ArchivedCycleLayoutRoot: React.FC = observer(() => { // router const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // hooks const { fetchArchivedCycles, currentProjectArchivedCycleIds, loader } = useCycle(); // cycle filters hook const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useCycleFilter(); // derived values const totalArchivedCycles = currentProjectArchivedCycleIds?.length ?? 0; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-cycles" }); useSWR( workspaceSlug && projectId ? `ARCHIVED_CYCLES_${workspaceSlug.toString()}_${projectId.toString()}` : null, @@ -64,7 +67,11 @@ export const ArchivedCycleLayoutRoot: React.FC = observer(() => { )} {totalArchivedCycles === 0 ? (
- +
) : (
diff --git a/web/core/components/empty-state/empty-state.tsx b/web/core/components/empty-state/empty-state.tsx deleted file mode 100644 index faab4ebc290..00000000000 --- a/web/core/components/empty-state/empty-state.tsx +++ /dev/null @@ -1,194 +0,0 @@ -"use client"; - -import React from "react"; -import { observer } from "mobx-react"; -import Image from "next/image"; -import Link from "next/link"; - -import { useTheme } from "next-themes"; -// hooks -// components -import { Button, TButtonSizes, TButtonVariant } from "@plane/ui"; -// constant -import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state"; -// helpers -import { cn } from "@/helpers/common.helper"; -import { useUserPermissions } from "@/hooks/store"; -import { EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; -import { ComicBoxButton } from "./comic-box-button"; - -export type EmptyStateProps = { - size?: TButtonSizes; - type: EmptyStateType; - layout?: "screen-detailed" | "screen-simple"; - additionalPath?: string; - primaryButtonConfig?: { - size?: TButtonSizes; - variant?: TButtonVariant; - }; - primaryButtonOnClick?: () => void; - primaryButtonLink?: string; - secondaryButtonOnClick?: () => void; -}; - -export const EmptyState: React.FC = observer((props) => { - const { - size = "lg", - type, - layout = "screen-detailed", - additionalPath = "", - primaryButtonConfig = { - size: "lg", - variant: "primary", - }, - primaryButtonOnClick, - primaryButtonLink, - secondaryButtonOnClick, - } = props; - // store - const { allowPermissions } = useUserPermissions(); - // theme - const { resolvedTheme } = useTheme(); - - // if empty state type is not found - if (!EMPTY_STATE_DETAILS[type]) return null; - - // current empty state details - const { key, title, description, path, primaryButton, secondaryButton, accessType, access } = - EMPTY_STATE_DETAILS[type]; - // resolved empty state path - const resolvedEmptyStatePath = `${additionalPath && additionalPath !== "" ? `${path}${additionalPath}` : path}-${ - resolvedTheme === "light" ? "light" : "dark" - }.webp`; - // permission - const isEditingAllowed = - access && - accessType && - allowPermissions( - access, - accessType === "workspace" ? EUserPermissionsLevel.WORKSPACE : EUserPermissionsLevel.PROJECT - ); - const anyButton = primaryButton || secondaryButton; - - // primary button - const renderPrimaryButton = () => { - if (!primaryButton) return null; - - const commonProps = { - size: primaryButtonConfig.size, - variant: primaryButtonConfig.variant, - prependIcon: primaryButton.icon, - onClick: primaryButtonOnClick ? primaryButtonOnClick : undefined, - disabled: !isEditingAllowed, - }; - - if (primaryButton.comicBox) { - return ( - - ); - } else if (primaryButtonLink) { - return ( - - - - ); - } else { - return ; - } - }; - // secondary button - const renderSecondaryButton = () => { - if (!secondaryButton) return null; - - return ( - - ); - }; - - return ( - <> - {layout === "screen-detailed" && ( -
-
-
- {description ? ( - <> -

{title}

-

{description}

- - ) : ( -

{title}

- )} -
- - {path && ( - {key - )} - - {anyButton && ( -
- {renderPrimaryButton()} - {renderSecondaryButton()} -
- )} -
-
- )} - {layout === "screen-simple" && ( -
-
- {key -
- {description ? ( - <> -

{title}

-

{description}

- - ) : ( -

{title}

- )} - {anyButton && ( -
- {renderPrimaryButton()} - {renderSecondaryButton()} -
- )} -
- )} - - ); -}); diff --git a/web/core/components/empty-state/index.ts b/web/core/components/empty-state/index.ts index afa892f2712..3ba1c3ad302 100644 --- a/web/core/components/empty-state/index.ts +++ b/web/core/components/empty-state/index.ts @@ -1,4 +1,3 @@ -export * from "./empty-state"; export * from "./helper"; export * from "./comic-box-button"; export * from "./detailed-empty-state-root"; diff --git a/web/core/components/empty-state/simple-empty-state-root.tsx b/web/core/components/empty-state/simple-empty-state-root.tsx index d0ab95076a7..ce00a143f9d 100644 --- a/web/core/components/empty-state/simple-empty-state-root.tsx +++ b/web/core/components/empty-state/simple-empty-state-root.tsx @@ -6,7 +6,7 @@ import Image from "next/image"; // utils import { cn } from "@plane/utils"; -type EmptyStateSize = "sm" | "md" | "lg"; +type EmptyStateSize = "sm" | "lg"; type Props = { title: string; @@ -17,12 +17,8 @@ type Props = { const sizeConfig = { sm: { - container: "size-20", - dimensions: 78, - }, - md: { container: "size-24", - dimensions: 80, + dimensions: 78, }, lg: { container: "size-28", diff --git a/web/core/components/exporter/guide.tsx b/web/core/components/exporter/guide.tsx index 7ee2899956a..65e77a6f200 100644 --- a/web/core/components/exporter/guide.tsx +++ b/web/core/components/exporter/guide.tsx @@ -8,19 +8,20 @@ import { useParams, useSearchParams } from "next/navigation"; import useSWR, { mutate } from "swr"; // icons import { MoveLeft, MoveRight, RefreshCw } from "lucide-react"; -// ui +// plane imports +import { useTranslation } from "@plane/i18n"; import { Button } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { Exporter, SingleExport } from "@/components/exporter"; import { ImportExportSettingsLoader } from "@/components/ui"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { EXPORT_SERVICES_LIST } from "@/constants/fetch-keys"; import { EXPORTERS_LIST } from "@/constants/workspace"; // hooks import { useProject, useUser, useUserPermissions } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; // services import { IntegrationService } from "@/services/integrations"; @@ -37,11 +38,14 @@ const IntegrationGuide = observer(() => { const { workspaceSlug } = useParams(); const searchParams = useSearchParams(); const provider = searchParams.get("provider"); + // plane hooks + const { t } = useTranslation(); // store hooks const { data: currentUser, canPerformAnyCreateAction } = useUser(); const { allowPermissions } = useUserPermissions(); - const { workspaceProjectIds } = useProject(); + // derived values + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/workspace-settings/exports" }); const { data: exporterServices } = useSWR( workspaceSlug && cursor ? EXPORT_SERVICES_LIST(workspaceSlug as string, cursor, `${per_page}`) : null, @@ -164,7 +168,11 @@ const IntegrationGuide = observer(() => {
) : (
- +
) ) : ( diff --git a/web/core/components/home/home-dashboard-widgets.tsx b/web/core/components/home/home-dashboard-widgets.tsx index 8ebdb70664e..1d0be0c5b03 100644 --- a/web/core/components/home/home-dashboard-widgets.tsx +++ b/web/core/components/home/home-dashboard-widgets.tsx @@ -1,14 +1,14 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// plane types +// plane imports +import { useTranslation } from "@plane/i18n"; import { THomeWidgetKeys, THomeWidgetProps } from "@plane/types"; // components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useHome } from "@/hooks/store/use-home"; // plane web components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { HomePageHeader } from "@/plane-web/components/home/header"; import { StickiesWidget } from "../stickies"; import { RecentActivityWidget } from "./widgets"; @@ -52,8 +52,12 @@ export const HOME_WIDGETS_LIST: { export const DashboardWidgets = observer(() => { // router const { workspaceSlug } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleWidgetSettings, widgetsMap, showWidgetSettings, orderedWidgets, isAnyWidgetEnabled } = useHome(); + // derived values + const noWidgetsResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/dashboard/widgets" }); if (!workspaceSlug) return null; @@ -80,14 +84,10 @@ export const DashboardWidgets = observer(() => { ) : (
- toggleWidgetSettings(true)} - primaryButtonConfig={{ - size: "sm", - variant: "neutral-primary", - }} +
)} diff --git a/web/core/components/inbox/modals/select-duplicate.tsx b/web/core/components/inbox/modals/select-duplicate.tsx index 9bcefad490a..ccd95593148 100644 --- a/web/core/components/inbox/modals/select-duplicate.tsx +++ b/web/core/components/inbox/modals/select-duplicate.tsx @@ -4,18 +4,16 @@ import React, { useEffect, useState } from "react"; import { useParams } from "next/navigation"; import { Search } from "lucide-react"; import { Combobox, Dialog, Transition } from "@headlessui/react"; -// icons -// components -// types +// plane imports +import { useTranslation } from "@plane/i18n"; import { ISearchIssueResponse } from "@plane/types"; -// ui import { Loader, TOAST_TYPE, setToast } from "@plane/ui"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +// components +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useProject } from "@/hooks/store"; import useDebounce from "@/hooks/use-debounce"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // services import { ProjectService } from "@/services/project"; @@ -30,18 +28,19 @@ const projectService = new ProjectService(); export const SelectDuplicateInboxIssueModal: React.FC = (props) => { const { isOpen, onClose, onSubmit, value } = props; - - const [query, setQuery] = useState(""); - + // router const { workspaceSlug, projectId, issueId } = useParams(); - - // hooks - const { getProjectById } = useProject(); - + // states + const [query, setQuery] = useState(""); const [issues, setIssues] = useState([]); const [isSearching, setIsSearching] = useState(false); - + // hooks + const { getProjectById } = useProject(); + const { t } = useTranslation(); + // derived values const debouncedSearchTerm: string = useDebounce(query, 500); + const searchResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/search" }); + const issuesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/issues" }); useEffect(() => { if (!isOpen || !workspaceSlug || !projectId) return; @@ -110,12 +109,11 @@ export const SelectDuplicateInboxIssueModal: React.FC = (props) => { ) : (
- + {query === "" ? ( + + ) : ( + + )}
); diff --git a/web/core/components/inbox/root.tsx b/web/core/components/inbox/root.tsx index 5d5617bb394..cea33743e6a 100644 --- a/web/core/components/inbox/root.tsx +++ b/web/core/components/inbox/root.tsx @@ -1,18 +1,19 @@ import { FC, useEffect, useState } from "react"; import { observer } from "mobx-react"; import { PanelLeft } from "lucide-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { Intake } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; import { InboxSidebar, InboxContentRoot } from "@/components/inbox"; import { InboxLayoutLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; // hooks import { useProjectInbox } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; type TInboxIssueRoot = { workspaceSlug: string; @@ -26,8 +27,12 @@ export const InboxIssueRoot: FC = observer((props) => { const { workspaceSlug, projectId, inboxIssueId, inboxAccessible, navigationTab } = props; // states const [isMobileSidebar, setIsMobileSidebar] = useState(true); + // plane hooks + const { t } = useTranslation(); // hooks const { loader, error, currentTab, handleCurrentTab, fetchInboxIssues } = useProjectInbox(); + // derived values + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/intake/issue-detail" }); useEffect(() => { if (!inboxAccessible || !workspaceSlug || !projectId) return; @@ -96,7 +101,7 @@ export const InboxIssueRoot: FC = observer((props) => { /> ) : (
- +
)} diff --git a/web/core/components/inbox/sidebar/root.tsx b/web/core/components/inbox/sidebar/root.tsx index b8c86e0dee5..ea85b5dd4b6 100644 --- a/web/core/components/inbox/sidebar/root.tsx +++ b/web/core/components/inbox/sidebar/root.tsx @@ -2,14 +2,14 @@ import { FC, useCallback, useEffect, useRef, useState } from "react"; import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { TInboxIssueCurrentTab } from "@plane/types"; import { Header, Loader, EHeaderVariant } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; import { FiltersRoot, InboxIssueAppliedFilters, InboxIssueList } from "@/components/inbox"; import { InboxSidebarLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; @@ -17,6 +17,7 @@ import { EInboxIssueCurrentTab } from "@/helpers/inbox.helper"; import { useProject, useProjectInbox } from "@/hooks/store"; import { useAppRouter } from "@/hooks/use-app-router"; import { useIntersectionObserver } from "@/hooks/use-intersection-observer"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; type IInboxSidebarProps = { workspaceSlug: string; @@ -38,9 +39,13 @@ const tabNavigationOptions: { key: TInboxIssueCurrentTab; label: string }[] = [ export const InboxSidebar: FC = observer((props) => { const { workspaceSlug, projectId, inboxIssueId, setIsMobileSidebar } = props; + // router + const router = useAppRouter(); // ref const containerRef = useRef(null); const [elementRef, setElementRef] = useState(null); + // plane hooks + const { t } = useTranslation(); // store const { currentProjectDetails } = useProject(); const { @@ -52,8 +57,11 @@ export const InboxSidebar: FC = observer((props) => { fetchInboxPaginationIssues, getAppliedFiltersCount, } = useProjectInbox(); - - const router = useAppRouter(); + // derived values + const sidebarAssetPath = useResolvedAssetPath({ basePath: "/empty-state/intake/intake-issue" }); + const sidebarFilterAssetPath = useResolvedAssetPath({ + basePath: "/empty-state/intake/filter-issue", + }); const fetchNextPages = useCallback(() => { if (!workspaceSlug || !projectId) return; @@ -128,16 +136,25 @@ export const InboxSidebar: FC = observer((props) => { /> ) : (
- 0 - ? EmptyStateType.INBOX_SIDEBAR_FILTER_EMPTY_STATE - : currentTab === EInboxIssueCurrentTab.OPEN - ? EmptyStateType.INBOX_SIDEBAR_OPEN_TAB - : EmptyStateType.INBOX_SIDEBAR_CLOSED_TAB - } - layout="screen-simple" - /> + {getAppliedFiltersCount > 0 ? ( + + ) : currentTab === EInboxIssueCurrentTab.OPEN ? ( + + ) : ( + + )}
)}
diff --git a/web/core/components/integration/guide.tsx b/web/core/components/integration/guide.tsx index da751cde8cf..e40085fce14 100644 --- a/web/core/components/integration/guide.tsx +++ b/web/core/components/integration/guide.tsx @@ -13,11 +13,9 @@ import { IImporterService } from "@plane/types"; // ui import { Button } from "@plane/ui"; // components -import { EmptyState } from "@/components/empty-state"; import { DeleteImportModal, GithubImporterRoot, JiraImporterRoot, SingleImport } from "@/components/integration"; import { ImportExportSettingsLoader } from "@/components/ui"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { IMPORTER_SERVICES_LIST } from "@/constants/fetch-keys"; import { IMPORTERS_LIST } from "@/constants/workspace"; // hooks @@ -28,6 +26,7 @@ import { IntegrationService } from "@/services/integrations"; // services const integrationService = new IntegrationService(); +// FIXME: [Deprecated] Remove this component const IntegrationGuide = observer(() => { // states const [refreshing, setRefreshing] = useState(false); @@ -137,7 +136,7 @@ const IntegrationGuide = observer(() => {
) : (
- + {/* */}
) ) : ( diff --git a/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx b/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx index 7b481af355b..8a7e9210845 100644 --- a/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx +++ b/web/core/components/issues/issue-detail/issue-activity/comments/root.tsx @@ -1,15 +1,15 @@ import { FC } from "react"; import { observer } from "mobx-react"; +// plane imports +import { useTranslation } from "@plane/i18n"; // components -import { EmptyState } from "@/components/empty-state"; -// hooks -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // hooks import { useIssueDetail } from "@/hooks/store"; -// components +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +// local components import { TActivityOperations } from "../root"; import { IssueCommentCard } from "./comment-card"; -// types type TIssueCommentRoot = { projectId: string; @@ -26,6 +26,9 @@ export const IssueCommentRoot: FC = observer((props) => { const { comment: { getCommentsByIssueId }, } = useIssueDetail(); + const { t } = useTranslation(); + // derived values + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/comments" }); const commentIds = getCommentsByIssueId(issueId); if (!commentIds) return <>; @@ -48,7 +51,11 @@ export const IssueCommentRoot: FC = observer((props) => { )) ) : (
- +
)} diff --git a/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx b/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx index 833a2d6054e..b4c3f7ae55c 100644 --- a/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/archived-issues.tsx @@ -1,31 +1,46 @@ import size from "lodash/size"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -import { EIssueFilterType, EIssuesStoreType } from "@plane/constants"; +// plane imports +import { EIssueFilterType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { IIssueFilterOptions } from "@plane/types"; -// hooks // components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; -import { useIssues } from "@/hooks/store"; -// types +import { DetailedEmptyState } from "@/components/empty-state"; +// hooks +import { useIssues, useUserPermissions } from "@/hooks/store"; +import { useAppRouter } from "@/hooks/use-app-router"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ProjectArchivedEmptyState: React.FC = observer(() => { // router + const router = useAppRouter(); const { workspaceSlug, projectId } = useParams(); - // theme + // plane hooks + const { t } = useTranslation(); // store hooks const { issuesFilter } = useIssues(EIssuesStoreType.ARCHIVED); - + const { allowPermissions } = useUserPermissions(); + // derived values const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) ) ); + const additionalPath = issueFilterCount > 0 ? (activeLayout ?? "list") : undefined; + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const emptyFilterResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/empty-filters/", + additionalPath: additionalPath, + }); + const archivedIssuesResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/archived/empty-issues", + }); const handleClearAllFilters = () => { if (!workspaceSlug || !projectId) return; @@ -38,20 +53,30 @@ export const ProjectArchivedEmptyState: React.FC = observer(() => { }); }; - const emptyStateType = - issueFilterCount > 0 ? EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER : EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES; - const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; - return (
- 0 ? undefined : `/${workspaceSlug}/projects/${projectId}/settings/automations` - } - secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} - /> + {issueFilterCount > 0 ? ( + + ) : ( + router.push(`/${workspaceSlug}/projects/${projectId}/settings/automations`), + disabled: !canPerformEmptyStateActions, + }} + /> + )}
); }); diff --git a/web/core/components/issues/issue-layouts/empty-states/cycle.tsx b/web/core/components/issues/issue-layouts/empty-states/cycle.tsx index 274aa85883b..3a2ac22ef40 100644 --- a/web/core/components/issues/issue-layouts/empty-states/cycle.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/cycle.tsx @@ -5,32 +5,58 @@ import isEmpty from "lodash/isEmpty"; import size from "lodash/size"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// types -import { EIssueFilterType, EIssuesStoreType } from "@plane/constants"; +// plane imports +import { EIssueFilterType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { IIssueFilterOptions, ISearchIssueResponse } from "@plane/types"; -// ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ExistingIssuesListModal } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; -import { useCommandPalette, useCycle, useEventTracker, useIssues } from "@/hooks/store"; +import { DetailedEmptyState } from "@/components/empty-state"; +import { useCommandPalette, useCycle, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const CycleEmptyState: React.FC = observer(() => { // router const { workspaceSlug, projectId, cycleId } = useParams(); // states const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false); + // plane hooks + const { t } = useTranslation(); // store hooks const { getCycleById } = useCycle(); const { issues, issuesFilter } = useIssues(EIssuesStoreType.CYCLE); const { toggleCreateIssueModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - + const { allowPermissions } = useUserPermissions(); + // derived values const cycleDetails = cycleId ? getCycleById(cycleId.toString()) : undefined; const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; + const issueFilterCount = size( + Object.fromEntries( + Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) + ) + ); + const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {}); + const isEmptyFilters = issueFilterCount > 0; + const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status?.toLowerCase() === "completed"; + const additionalPath = activeLayout ?? "list"; + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const emptyFilterResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/empty-filters/", + additionalPath: additionalPath, + }); + const noIssueResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/cycle-issues/", + additionalPath: additionalPath, + }); + const completedNoIssuesResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/cycle/completed-no-issues", + }); const handleAddIssuesToCycle = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -54,13 +80,6 @@ export const CycleEmptyState: React.FC = observer(() => { }) ); }; - const issueFilterCount = size( - Object.fromEntries( - Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) - ) - ); - - const isCompletedCycleSnapshotAvailable = !isEmpty(cycleDetails?.progress_snapshot ?? {}); const handleClearAllFilters = () => { if (!workspaceSlug || !projectId || !cycleId) return; @@ -79,16 +98,6 @@ export const CycleEmptyState: React.FC = observer(() => { ); }; - const isEmptyFilters = issueFilterCount > 0; - const isCompletedAndEmpty = isCompletedCycleSnapshotAvailable || cycleDetails?.status?.toLowerCase() === "completed"; - - const emptyStateType = isCompletedAndEmpty - ? EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES - : isEmptyFilters - ? EmptyStateType.PROJECT_EMPTY_FILTER - : EmptyStateType.PROJECT_CYCLE_NO_ISSUES; - const additionalPath = isCompletedAndEmpty ? undefined : activeLayout ?? "list"; - return (
{ handleOnSubmit={handleAddIssuesToCycle} />
- { - setTrackElement("Cycle issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); - } - : undefined - } - secondaryButtonOnClick={ - !isCompletedAndEmpty && isEmptyFilters ? handleClearAllFilters : () => setCycleIssuesListModal(true) - } - /> + {isCompletedAndEmpty ? ( + + ) : isEmptyFilters ? ( + + ) : ( + { + setTrackElement("Cycle issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.CYCLE); + }, + disabled: !canPerformEmptyStateActions, + }} + secondaryButton={{ + text: t("project_cycles.empty_state.no_issues.secondary_button.text"), + onClick: () => setCycleIssuesListModal(true), + disabled: !canPerformEmptyStateActions, + }} + /> + )}
); diff --git a/web/core/components/issues/issue-layouts/empty-states/draft-issues.tsx b/web/core/components/issues/issue-layouts/empty-states/draft-issues.tsx index 1af3ca5a5c4..bb10176ae9f 100644 --- a/web/core/components/issues/issue-layouts/empty-states/draft-issues.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/draft-issues.tsx @@ -1,53 +1,6 @@ -import size from "lodash/size"; import { observer } from "mobx-react"; -import { useParams } from "next/navigation"; -import { EIssueFilterType, EIssuesStoreType } from "@plane/constants"; -import { IIssueFilterOptions } from "@plane/types"; -// hooks -// components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; -import { useIssues } from "@/hooks/store"; -// types -export const ProjectDraftEmptyState: React.FC = observer(() => { - // router - const { workspaceSlug, projectId } = useParams(); - // store hooks - const { issuesFilter } = useIssues(EIssuesStoreType.DRAFT); - - const userFilters = issuesFilter?.issueFilters?.filters; - const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - - const issueFilterCount = size( - Object.fromEntries( - Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) - ) - ); - - const handleClearAllFilters = () => { - if (!workspaceSlug || !projectId) return; - const newFilters: IIssueFilterOptions = {}; - Object.keys(userFilters ?? {}).forEach((key) => { - newFilters[key as keyof IIssueFilterOptions] = []; - }); - issuesFilter.updateFilters(workspaceSlug.toString(), projectId.toString(), EIssueFilterType.FILTERS, { - ...newFilters, - }); - }; - - const emptyStateType = - issueFilterCount > 0 ? EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER : EmptyStateType.PROJECT_DRAFT_NO_ISSUES; - const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; - - return ( -
- 0 ? handleClearAllFilters : undefined} - /> -
- ); -}); +// FIXME: Project drafts is deprecated. Remove this component and all the related code. +export const ProjectDraftEmptyState: React.FC = observer(() => ( +
+)); diff --git a/web/core/components/issues/issue-layouts/empty-states/global-view.tsx b/web/core/components/issues/issue-layouts/empty-states/global-view.tsx index 8850fe22677..6eb8543d9e7 100644 --- a/web/core/components/issues/issue-layouts/empty-states/global-view.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/global-view.tsx @@ -1,43 +1,77 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +// plane imports +import { EIssuesStoreType, EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; // components -import { EIssuesStoreType } from "@plane/constants"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EMPTY_STATE_DETAILS, EmptyStateType } from "@/constants/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; // hooks -import { useCommandPalette, useEventTracker, useProject } from "@/hooks/store"; -// assets +import { useCommandPalette, useEventTracker, useProject, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const GlobalViewEmptyState: React.FC = observer(() => { const { globalViewId } = useParams(); + // plane imports + const { t } = useTranslation(); // store hooks const { workspaceProjectIds } = useProject(); const { toggleCreateIssueModal, toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - + const { allowPermissions } = useUserPermissions(); + // derived values + const hasMemberLevelPermission = allowPermissions( + [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); const isDefaultView = ["all-issues", "assigned", "created", "subscribed"].includes(globalViewId?.toString() ?? ""); const currentView = isDefaultView && globalViewId ? globalViewId : "custom-view"; + const resolvedCurrentView = currentView?.toString(); + const noProjectResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); + const globalViewsResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/all-issues/", + additionalPath: resolvedCurrentView, + }); - const emptyStateType = - (workspaceProjectIds ?? []).length > 0 ? `workspace-${currentView}` : EmptyStateType.WORKSPACE_NO_PROJECTS; + if (workspaceProjectIds?.length === 0) { + return ( + { + setTrackElement("All issues empty state"); + toggleCreateProjectModal(true); + }} + disabled={!hasMemberLevelPermission} + /> + } + /> + ); + } return ( - 0 - ? currentView !== "custom-view" && currentView !== "subscribed" - ? () => { + title={t(`workspace_issues.empty_state.${resolvedCurrentView}.title`)} + description={t(`workspace_issues.empty_state.${resolvedCurrentView}.description`)} + assetPath={globalViewsResolvedPath} + primaryButton={ + ["subscribed", "custom-view"].includes(resolvedCurrentView) === false + ? { + text: t(`workspace_issues.empty_state.${resolvedCurrentView}.primary_button.text`), + onClick: () => { setTrackElement("All issues empty state"); toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - } - : undefined - : () => { - setTrackElement("All issues empty state"); - toggleCreateProjectModal(true); + }, + disabled: !hasMemberLevelPermission, } + : undefined } /> ); diff --git a/web/core/components/issues/issue-layouts/empty-states/module.tsx b/web/core/components/issues/issue-layouts/empty-states/module.tsx index 9e8a26955e6..517542b44ec 100644 --- a/web/core/components/issues/issue-layouts/empty-states/module.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/module.tsx @@ -4,31 +4,52 @@ import { useState } from "react"; import size from "lodash/size"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// types -import { EIssueFilterType, EIssuesStoreType } from "@plane/constants"; +// plane imports +import { EIssueFilterType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { IIssueFilterOptions, ISearchIssueResponse } from "@plane/types"; -// ui import { TOAST_TYPE, setToast } from "@plane/ui"; // components import { ExistingIssuesListModal } from "@/components/core"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; // hooks -import { useCommandPalette, useEventTracker, useIssues } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ModuleEmptyState: React.FC = observer(() => { // router const { workspaceSlug, projectId, moduleId } = useParams(); // states const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false); + // plane hooks + const { t } = useTranslation(); // store hooks const { issues, issuesFilter } = useIssues(EIssuesStoreType.MODULE); const { toggleCreateIssueModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - + const { allowPermissions } = useUserPermissions(); + // derived values const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; + const issueFilterCount = size( + Object.fromEntries( + Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) + ) + ); + const isEmptyFilters = issueFilterCount > 0; + const additionalPath = activeLayout ?? "list"; + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const emptyFilterResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/empty-filters/", + additionalPath: additionalPath, + }); + const moduleIssuesResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/module-issues/", + additionalPath: additionalPath, + }); const handleAddIssuesToModule = async (data: ISearchIssueResponse[]) => { if (!workspaceSlug || !projectId || !moduleId) return; @@ -52,12 +73,6 @@ export const ModuleEmptyState: React.FC = observer(() => { ); }; - const issueFilterCount = size( - Object.fromEntries( - Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) - ) - ); - const handleClearAllFilters = () => { if (!workspaceSlug || !projectId || !moduleId) return; const newFilters: IIssueFilterOptions = {}; @@ -75,10 +90,6 @@ export const ModuleEmptyState: React.FC = observer(() => { ); }; - const isEmptyFilters = issueFilterCount > 0; - const emptyStateType = isEmptyFilters ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_MODULE_ISSUES; - const additionalPath = activeLayout ?? "list"; - return (
{ handleOnSubmit={handleAddIssuesToModule} />
- { - setTrackElement("Module issue empty state"); - toggleCreateIssueModal(true, EIssuesStoreType.MODULE); - } - } - secondaryButtonOnClick={isEmptyFilters ? handleClearAllFilters : () => setModuleIssuesListModal(true)} - /> + {isEmptyFilters ? ( + + ) : ( + { + setTrackElement("Module issue empty state"); + toggleCreateIssueModal(true, EIssuesStoreType.MODULE); + }, + disabled: !canPerformEmptyStateActions, + }} + secondaryButton={{ + text: t("project_module.empty_state.no_issues.secondary_button.text"), + onClick: () => setModuleIssuesListModal(true), + disabled: !canPerformEmptyStateActions, + }} + /> + )}
); diff --git a/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx b/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx index 12a31df838f..d67aad491f9 100644 --- a/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/profile-view.tsx @@ -1,19 +1,30 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components -import { EmptyState } from "@/components/empty-state"; +import { useTranslation } from "@plane/i18n"; +import { DetailedEmptyState } from "@/components/empty-state"; // constants -import { EMPTY_STATE_DETAILS } from "@/constants/empty-state"; - -// assets +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +// TODO: If projectViewId changes, everything breaks. Figure out a better way to handle this. export const ProfileViewEmptyState: React.FC = observer(() => { + // plane hooks + const { t } = useTranslation(); // store hooks const { profileViewId } = useParams(); + // derived values + const resolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/profile/", + additionalPath: profileViewId?.toString(), + }); if (!profileViewId) return null; - const emptyStateType = `profile-${profileViewId.toString()}`; - - return ; + return ( + + ); }); diff --git a/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx b/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx index 213e5ac405c..e8bf3a9d565 100644 --- a/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/project-epic.tsx @@ -1,12 +1 @@ -// types -// components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; -// hooks - -export const ProjectEpicsEmptyState: React.FC = () => ( -
- {}} /> -
-); +export const ProjectEpicsEmptyState: React.FC = () => <>; diff --git a/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx b/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx index 5e437357a89..109b67c28d8 100644 --- a/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx +++ b/web/core/components/issues/issue-layouts/empty-states/project-issues.tsx @@ -1,33 +1,46 @@ import size from "lodash/size"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// types -import { EIssueFilterType, EIssuesStoreType } from "@plane/constants"; +// plane imports +import { EIssueFilterType, EIssuesStoreType, EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { IIssueFilterOptions } from "@plane/types"; // components -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; // hooks -import { useCommandPalette, useEventTracker, useIssues } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useIssues, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ProjectEmptyState: React.FC = observer(() => { // router const { workspaceSlug, projectId } = useParams(); + // plane imports + const { t } = useTranslation(); // store hooks const { toggleCreateIssueModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); - const { issuesFilter } = useIssues(EIssuesStoreType.PROJECT); - + const { allowPermissions } = useUserPermissions(); + // derived values const userFilters = issuesFilter?.issueFilters?.filters; const activeLayout = issuesFilter?.issueFilters?.displayFilters?.layout; - const issueFilterCount = size( Object.fromEntries( Object.entries(userFilters ?? {}).filter(([, value]) => value && Array.isArray(value) && value.length > 0) ) ); + const additionalPath = issueFilterCount > 0 ? (activeLayout ?? "list") : undefined; + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const emptyFilterResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/empty-filters/", + additionalPath: additionalPath, + }); + const projectIssuesResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/onboarding/issues", + }); const handleClearAllFilters = () => { if (!workspaceSlug || !projectId) return; @@ -40,24 +53,37 @@ export const ProjectEmptyState: React.FC = observer(() => { }); }; - const emptyStateType = issueFilterCount > 0 ? EmptyStateType.PROJECT_EMPTY_FILTER : EmptyStateType.PROJECT_NO_ISSUES; - const additionalPath = issueFilterCount > 0 ? activeLayout ?? "list" : undefined; - return (
- 0 - ? undefined - : () => { + {issueFilterCount > 0 ? ( + + ) : ( + { setTrackElement("Project issue empty state"); toggleCreateIssueModal(true, EIssuesStoreType.PROJECT); - } - } - secondaryButtonOnClick={issueFilterCount > 0 ? handleClearAllFilters : undefined} - /> + }} + disabled={!canPerformEmptyStateActions} + /> + } + /> + )}
); }); diff --git a/web/core/components/issues/workspace-draft/empty-state.tsx b/web/core/components/issues/workspace-draft/empty-state.tsx index 7a2893abd98..1c0b51fbb1d 100644 --- a/web/core/components/issues/workspace-draft/empty-state.tsx +++ b/web/core/components/issues/workspace-draft/empty-state.tsx @@ -2,15 +2,27 @@ import { FC, Fragment, useState } from "react"; // components -import { EIssuesStoreType } from "@plane/constants"; -import { EmptyState } from "@/components/empty-state"; +import { observer } from "mobx-react"; +import { EIssuesStoreType, EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; +import { DetailedEmptyState } from "@/components/empty-state"; import { CreateUpdateIssueModal } from "@/components/issues"; // constants -import { EmptyStateType } from "@/constants/empty-state"; +import { useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; -export const WorkspaceDraftEmptyState: FC = () => { +export const WorkspaceDraftEmptyState: FC = observer(() => { // state const [isDraftIssueModalOpen, setIsDraftIssueModalOpen] = useState(false); + // store hooks + const { t } = useTranslation(); + const { allowPermissions } = useUserPermissions(); + // derived values + const canPerformEmptyStateActions = allowPermissions( + [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/cycles" }); return ( @@ -21,13 +33,19 @@ export const WorkspaceDraftEmptyState: FC = () => { isDraft />
- { - setIsDraftIssueModalOpen(true); + { + setIsDraftIssueModalOpen(true); + }, + disabled: !canPerformEmptyStateActions }} />
); -}; +}); diff --git a/web/core/components/issues/workspace-draft/root.tsx b/web/core/components/issues/workspace-draft/root.tsx index 633fdead729..30f99bde671 100644 --- a/web/core/components/issues/workspace-draft/root.tsx +++ b/web/core/components/issues/workspace-draft/root.tsx @@ -3,15 +3,18 @@ import { FC, Fragment } from "react"; import { observer } from "mobx-react"; import useSWR from "swr"; +// plane imports +import { EUserPermissionsLevel, EUserWorkspaceRoles } from "@plane/constants/src/user"; +import { useTranslation } from "@plane/i18n"; // components -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { EDraftIssuePaginationType } from "@/constants/workspace-drafts"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useCommandPalette, useProject, useWorkspaceDraftIssues } from "@/hooks/store"; +import { useCommandPalette, useProject, useUserPermissions, useWorkspaceDraftIssues } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { useWorkspaceIssueProperties } from "@/hooks/use-workspace-issue-properties"; // components import { DraftIssueBlock } from "./draft-issue-block"; @@ -24,10 +27,19 @@ type TWorkspaceDraftIssuesRoot = { export const WorkspaceDraftIssuesRoot: FC = observer((props) => { const { workspaceSlug } = props; + // plane hooks + const { t } = useTranslation(); // hooks const { loader, paginationInfo, fetchIssues, issueIds } = useWorkspaceDraftIssues(); const { workspaceProjectIds } = useProject(); const { toggleCreateProjectModal } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); + // derived values + const hasMemberLevelPermission = allowPermissions( + [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + const noProjectResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); //swr hook for fetching issue properties useWorkspaceIssueProperties(workspaceSlug); @@ -51,12 +63,22 @@ export const WorkspaceDraftIssuesRoot: FC = observer( if (workspaceProjectIds?.length === 0) return ( - { - toggleCreateProjectModal(true); - }} + title={t("workspace_projects.empty_state.no_projects.title")} + description={t("workspace_projects.empty_state.no_projects.description")} + assetPath={noProjectResolvedPath} + customPrimaryButton={ + { + toggleCreateProjectModal(true); + }} + disabled={!hasMemberLevelPermission} + /> + } /> ); diff --git a/web/core/components/labels/project-setting-label-list.tsx b/web/core/components/labels/project-setting-label-list.tsx index 2fc9c838277..e0943da3769 100644 --- a/web/core/components/labels/project-setting-label-list.tsx +++ b/web/core/components/labels/project-setting-label-list.tsx @@ -3,39 +3,40 @@ import React, { useState, useRef } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +// plane imports +import { useTranslation } from "@plane/i18n"; import { IIssueLabel } from "@plane/types"; -// hooks import { Button, Loader } from "@plane/ui"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { CreateUpdateLabelInline, DeleteLabelModal, ProjectSettingLabelGroup, ProjectSettingLabelItem, } from "@/components/labels"; -import { EmptyStateType } from "@/constants/empty-state"; +// hooks import { useLabel, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +// plane web imports import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions"; -// components -// ui -// types -// constants export const ProjectSettingsLabelList: React.FC = observer(() => { + // router + const { workspaceSlug, projectId } = useParams(); + // refs + const scrollToRef = useRef(null); // states const [showLabelForm, setLabelForm] = useState(false); const [isUpdating, setIsUpdating] = useState(false); const [selectDeleteLabel, setSelectDeleteLabel] = useState(null); - // refs - const scrollToRef = useRef(null); - // router - const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { projectLabels, updateLabelPosition, projectLabelsTree } = useLabel(); const { allowPermissions } = useUserPermissions(); - // derived values const isEditable = allowPermissions([EUserPermissions.ADMIN], EUserPermissionsLevel.PROJECT); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/project-settings/labels" }); const newLabel = () => { setIsUpdating(false); @@ -94,7 +95,11 @@ export const ProjectSettingsLabelList: React.FC = observer(() => { {projectLabels ? ( projectLabels.length === 0 && !showLabelForm ? (
- +
) : ( projectLabelsTree && ( diff --git a/web/core/components/modules/archived-modules/root.tsx b/web/core/components/modules/archived-modules/root.tsx index b74814add67..30ae74a5ce8 100644 --- a/web/core/components/modules/archived-modules/root.tsx +++ b/web/core/components/modules/archived-modules/root.tsx @@ -2,28 +2,30 @@ import React, { useCallback } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; -// types +// plane imports +import { useTranslation } from "@plane/i18n"; import { TModuleFilters } from "@plane/types"; // components -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { ArchivedModulesView, ModuleAppliedFiltersList } from "@/components/modules"; import { CycleModuleListLayout } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // helpers import { calculateTotalFilters } from "@/helpers/filter.helper"; // hooks import { useModule, useModuleFilter } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ArchivedModuleLayoutRoot: React.FC = observer(() => { // router const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // hooks const { fetchArchivedModules, projectArchivedModuleIds, loader } = useModule(); - // module filters hook const { clearAllFilters, currentProjectArchivedFilters, updateFilters } = useModuleFilter(); // derived values const totalArchivedModules = projectArchivedModuleIds?.length ?? 0; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/archived/empty-modules" }); useSWR( workspaceSlug && projectId ? `ARCHIVED_MODULES_${workspaceSlug.toString()}_${projectId.toString()}` : null, @@ -69,7 +71,11 @@ export const ArchivedModuleLayoutRoot: React.FC = observer(() => { )} {totalArchivedModules === 0 ? (
- +
) : (
diff --git a/web/core/components/modules/modules-list-view.tsx b/web/core/components/modules/modules-list-view.tsx index 4b45879f7a2..4d8eac8d3c1 100644 --- a/web/core/components/modules/modules-list-view.tsx +++ b/web/core/components/modules/modules-list-view.tsx @@ -2,15 +2,16 @@ import { observer } from "mobx-react"; import Image from "next/image"; import { useParams, useSearchParams } from "next/navigation"; // components +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ContentWrapper, Row, ERowVariant } from "@plane/ui"; import { ListLayout } from "@/components/core/list"; -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState, ComicBoxButton } from "@/components/empty-state"; import { ModuleCardItem, ModuleListItem, ModulePeekOverview, ModulesListGanttChartView } from "@/components/modules"; import { CycleModuleBoardLayout, CycleModuleListLayout, GanttLayoutLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useCommandPalette, useEventTracker, useModule, useModuleFilter } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useModule, useModuleFilter, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import AllFiltersImage from "@/public/empty-state/module/all-filters.svg"; import NameFilterImage from "@/public/empty-state/module/name-filter.svg"; @@ -19,14 +20,24 @@ export const ModulesListView: React.FC = observer(() => { const { workspaceSlug, projectId } = useParams(); const searchParams = useSearchParams(); const peekModule = searchParams.get("peekModule"); + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleCreateModuleModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); const { getProjectModuleIds, getFilteredModuleIds, loader } = useModule(); const { currentProjectDisplayFilters: displayFilters, searchQuery } = useModuleFilter(); + const { allowPermissions } = useUserPermissions(); // derived values const projectModuleIds = projectId ? getProjectModuleIds(projectId.toString()) : undefined; const filteredModuleIds = projectId ? getFilteredModuleIds(projectId.toString()) : undefined; + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const generalViewResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/onboarding/modules", + }); if (loader || !projectModuleIds || !filteredModuleIds) return ( @@ -39,12 +50,22 @@ export const ModulesListView: React.FC = observer(() => { if (projectModuleIds.length === 0) return ( - { - setTrackElement("Module empty state"); - toggleCreateModuleModal(true); - }} + { + setTrackElement("Module empty state"); + toggleCreateModuleModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } /> ); diff --git a/web/core/components/page-views/workspace-dashboard.tsx b/web/core/components/page-views/workspace-dashboard.tsx index f4df757cd8b..b20ed914b5a 100644 --- a/web/core/components/page-views/workspace-dashboard.tsx +++ b/web/core/components/page-views/workspace-dashboard.tsx @@ -1,23 +1,37 @@ import { useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; -// components +// plane imports +import { EUserPermissionsLevel } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ContentWrapper } from "@plane/ui"; +// components import { DashboardWidgets } from "@/components/dashboard"; -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { IssuePeekOverview } from "@/components/issues"; import { TourRoot } from "@/components/onboarding"; import { UserGreetingsView } from "@/components/user"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { PRODUCT_TOUR_COMPLETED } from "@/constants/event-tracker"; // helpers import { cn } from "@/helpers/common.helper"; // hooks -import { useCommandPalette, useUserProfile, useEventTracker, useDashboard, useProject, useUser } from "@/hooks/store"; +import { + useCommandPalette, + useUserProfile, + useEventTracker, + useDashboard, + useProject, + useUser, + useUserPermissions, +} from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import useSize from "@/hooks/use-window-size"; +import { EUserPermissions } from "@/plane-web/constants"; export const WorkspaceDashboardView = observer(() => { + // plane hooks + const { t } = useTranslation(); // store hooks const { // captureEvent, @@ -30,8 +44,11 @@ export const WorkspaceDashboardView = observer(() => { const { captureEvent } = useEventTracker(); const { homeDashboardId, fetchHomeDashboardWidgets } = useDashboard(); const { joinedProjectIds, loader } = useProject(); + const { allowPermissions } = useUserPermissions(); + // helper hooks const [windowWidth] = useSize(); + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/dashboard" }); const handleTourCompleted = () => { updateTourCompleted() @@ -53,6 +70,11 @@ export const WorkspaceDashboardView = observer(() => { fetchHomeDashboardWidgets(workspaceSlug?.toString()); }, [fetchHomeDashboardWidgets, workspaceSlug]); + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + // TODO: refactor loader implementation return ( <> @@ -77,12 +99,22 @@ export const WorkspaceDashboardView = observer(() => { ) : ( - { - setTrackElement("Dashboard empty state"); - toggleCreateProjectModal(true); - }} + { + setTrackElement("Dashboard empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } /> )} diff --git a/web/core/components/pages/pages-list-main-content.tsx b/web/core/components/pages/pages-list-main-content.tsx index a0b4b356591..ec5ac519333 100644 --- a/web/core/components/pages/pages-list-main-content.tsx +++ b/web/core/components/pages/pages-list-main-content.tsx @@ -1,15 +1,16 @@ import { observer } from "mobx-react"; import Image from "next/image"; -// types +// plane imports +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { TPageNavigationTabs } from "@plane/types"; // components -import { EmptyState } from "@/components/empty-state"; +import { DetailedEmptyState } from "@/components/empty-state"; import { PageLoader } from "@/components/pages"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks import { EPageAccess } from "@/constants/page"; -import { useCommandPalette, useProjectPages } from "@/hooks/store"; +import { useCommandPalette, useProjectPages, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; // assets import AllFiltersImage from "@/public/empty-state/pages/all-filters.svg"; import NameFilterImage from "@/public/empty-state/pages/name-filter.svg"; @@ -21,46 +22,90 @@ type Props = { export const PagesListMainContent: React.FC = observer((props) => { const { children, pageType } = props; + // plane hooks + const { t } = useTranslation(); // store hooks const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIds, getCurrentProjectPageIds, filters } = useProjectPages(); const { toggleCreatePageModal } = useCommandPalette(); + const { allowPermissions } = useUserPermissions(); // derived values const pageIds = getCurrentProjectPageIds(pageType); const filteredPageIds = getCurrentProjectFilteredPageIds(pageType); + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER], + EUserPermissionsLevel.PROJECT + ); + const generalPageResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/onboarding/pages", + }); + const publicPageResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/pages/public", + }); + const privatePageResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/pages/private", + }); + const archivedPageResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/pages/archived", + }); if (loader === "init-loader") return ; // if no pages exist in the active page type if (!isAnyPageAvailable || pageIds?.length === 0) { if (!isAnyPageAvailable) { return ( - { - toggleCreatePageModal({ isOpen: true }); + { + toggleCreatePageModal({ isOpen: true }); + }, + disabled: !canPerformEmptyStateActions, }} /> ); } if (pageType === "public") return ( - { - toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC }); + { + toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC }); + }, + disabled: !canPerformEmptyStateActions, }} /> ); if (pageType === "private") return ( - { - toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE }); + { + toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE }); + }, + disabled: !canPerformEmptyStateActions, }} /> ); - if (pageType === "archived") return ; + if (pageType === "archived") + return ( + + ); } // if no pages match the filter criteria if (filteredPageIds?.length === 0) diff --git a/web/core/components/project/card-list.tsx b/web/core/components/project/card-list.tsx index ff98c188c55..3d0fa1fae74 100644 --- a/web/core/components/project/card-list.tsx +++ b/web/core/components/project/card-list.tsx @@ -1,14 +1,18 @@ import { observer } from "mobx-react"; import Image from "next/image"; -// components +// plane imports +import { EUserPermissionsLevel } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ContentWrapper } from "@plane/ui"; -import { EmptyState } from "@/components/empty-state"; +// components +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { ProjectCard } from "@/components/project"; import { ProjectsLoader } from "@/components/ui"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useCommandPalette, useEventTracker, useProject, useProjectFilter } from "@/hooks/store"; +import { useCommandPalette, useEventTracker, useProject, useProjectFilter, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; +// plane-web +import { EUserPermissions } from "@/plane-web/constants"; // assets import AllFiltersImage from "@/public/empty-state/project/all-filters.svg"; import NameFilterImage from "@/public/empty-state/project/name-filter.svg"; @@ -20,6 +24,8 @@ type TProjectCardListProps = { export const ProjectCardList = observer((props: TProjectCardListProps) => { const { totalProjectIds: totalProjectIdsProps, filteredProjectIds: filteredProjectIdsProps } = props; + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); @@ -30,20 +36,41 @@ export const ProjectCardList = observer((props: TProjectCardListProps) => { loader, } = useProject(); const { searchQuery, currentWorkspaceDisplayFilters } = useProjectFilter(); + const { allowPermissions } = useUserPermissions(); + + // helper hooks + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); + // derived values const workspaceProjectIds = totalProjectIdsProps ?? storeWorkspaceProjectIds; const filteredProjectIds = filteredProjectIdsProps ?? storeFilteredProjectIds; + // permissions + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + if (!filteredProjectIds || !workspaceProjectIds || loader) return ; if (workspaceProjectIds?.length === 0 && !currentWorkspaceDisplayFilters?.archived_projects) return ( - { - setTrackElement("Project empty state"); - toggleCreateProjectModal(true); - }} + { + setTrackElement("Project empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } /> ); diff --git a/web/core/components/project/multi-select-modal.tsx b/web/core/components/project/multi-select-modal.tsx index e866f2595e6..013cd1248bf 100644 --- a/web/core/components/project/multi-select-modal.tsx +++ b/web/core/components/project/multi-select-modal.tsx @@ -4,16 +4,16 @@ import { observer } from "mobx-react"; import { Search, X } from "lucide-react"; import { Combobox } from "@headlessui/react"; // plane ui +import { useTranslation } from "@plane/i18n"; import { Button, Checkbox, EModalPosition, EModalWidth, ModalCore } from "@plane/ui"; // components import { Logo } from "@/components/common"; -import { EmptyState } from "@/components/empty-state"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; +import { SimpleEmptyState } from "@/components/empty-state"; // helpers import { cn } from "@/helpers/common.helper"; // hooks import { useProject } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; type Props = { isOpen: boolean; @@ -31,6 +31,8 @@ export const ProjectMultiSelectModal: React.FC = observer((props) => { const [isSubmitting, setIsSubmitting] = useState(false); // refs const moveButtonRef = useRef(null); + // plane hooks + const { t } = useTranslation(); // store hooks const { getProjectById } = useProject(); // derived values @@ -44,6 +46,9 @@ export const ProjectMultiSelectModal: React.FC = observer((props) => { const projectQuery = `${project?.identifier} ${project?.name}`.toLowerCase(); return projectQuery.includes(searchTerm.toLowerCase()); }); + const filteredProjectResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/search/project", + }); useEffect(() => { if (isOpen) setSelectedProjectIds(selectedProjectIdsProp); @@ -114,7 +119,11 @@ export const ProjectMultiSelectModal: React.FC = observer((props) => { > {filteredProjectIds.length === 0 ? (
- +
) : (
    { const { workspaceSlug, intersectionElement, columnCount } = props; // navigation const pathname = usePathname(); + // plane hooks + const { t } = useTranslation(); // store hooks const { getWorkspaceStickyIds, toggleShowNewSticky, searchQuery, loader } = useSticky(); + const { allowPermissions } = useUserPermissions(); // sticky operations const { stickyOperations } = useStickyOperations({ workspaceSlug: workspaceSlug?.toString() }); // derived values @@ -41,6 +49,14 @@ export const StickiesList = observer((props: TProps) => { const itemWidth = `${100 / columnCount}%`; const totalRows = Math.ceil(workspaceStickyIds.length / columnCount); const isStickiesPage = pathname?.includes("stickies"); + const hasGuestLevelPermissions = allowPermissions( + [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST], + EUserPermissionsLevel.WORKSPACE + ); + const stickiesResolvedPath = useResolvedAssetPath({ basePath: "/empty-state/stickies/stickies" }); + const stickiesSearchResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/stickies/stickies-search", + }); const masonryRef = useRef(null); const handleLayout = () => { @@ -84,19 +100,32 @@ export const StickiesList = observer((props: TProps) => { if (loader === "loaded" && workspaceStickyIds.length === 0) { return ( -
    - {isStickiesPage || searchQuery ? ( - { - toggleShowNewSticky(true); - stickyOperations.create(); - }} - primaryButtonConfig={{ - size: "sm", - }} - /> +
    + {isStickiesPage ? ( + <> + {searchQuery ? ( + + ) : ( + , + text: t("stickies.empty_state.general.primary_button.text"), + onClick: () => { + toggleShowNewSticky(true); + stickyOperations.create(); + }, + disabled: !hasGuestLevelPermissions, + }} + /> + )} + ) : ( )} diff --git a/web/core/components/views/views-list.tsx b/web/core/components/views/views-list.tsx index 6b7d4117241..29a0c4cb08f 100644 --- a/web/core/components/views/views-list.tsx +++ b/web/core/components/views/views-list.tsx @@ -1,31 +1,49 @@ import { observer } from "mobx-react"; import { useParams } from "next/navigation"; +// plane imports +import { EUserPermissionsLevel, EUserProjectRoles } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; // components import { ListLayout } from "@/components/core/list"; -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState, SimpleEmptyState } from "@/components/empty-state"; import { ViewListLoader } from "@/components/ui"; import { ProjectViewListItem } from "@/components/views"; -// constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks -import { useCommandPalette, useProjectView } from "@/hooks/store"; -// assets +import { useCommandPalette, useProjectView, useUserPermissions } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const ProjectViewsList = observer(() => { const { projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleCreateViewModal } = useCommandPalette(); const { getProjectViews, getFilteredProjectViews, loader } = useProjectView(); - + const { allowPermissions } = useUserPermissions(); + // derived values const projectViews = getProjectViews(projectId?.toString()); const filteredProjectViews = getFilteredProjectViews(projectId?.toString()); + const canPerformEmptyStateActions = allowPermissions( + [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER, EUserProjectRoles.GUEST], + EUserPermissionsLevel.PROJECT + ); + const generalViewResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/onboarding/views", + }); + const filteredViewResolvedPath = useResolvedAssetPath({ + basePath: "/empty-state/search/views", + }); if (loader || !projectViews || !filteredProjectViews) return ; if (filteredProjectViews.length === 0 && projectViews.length > 0) { return (
    - +
    ); } @@ -43,7 +61,20 @@ export const ProjectViewsList = observer(() => {
    ) : ( - toggleCreateViewModal(true)} /> + toggleCreateViewModal(true)} + disabled={!canPerformEmptyStateActions} + /> + } + /> )} ); diff --git a/web/core/components/workspace-notifications/sidebar/empty-state.tsx b/web/core/components/workspace-notifications/sidebar/empty-state.tsx index 3b2b0c0f546..687e0c38c73 100644 --- a/web/core/components/workspace-notifications/sidebar/empty-state.tsx +++ b/web/core/components/workspace-notifications/sidebar/empty-state.tsx @@ -3,16 +3,33 @@ import { FC } from "react"; import { observer } from "mobx-react"; // components -import { EmptyState } from "@/components/empty-state"; +import { useTranslation } from "@plane/i18n"; +import { SimpleEmptyState } from "@/components/empty-state"; // constants -import { EmptyStateType } from "@/constants/empty-state"; import { ENotificationTab } from "@/constants/notification"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; export const NotificationEmptyState: FC = observer(() => { + // plane imports + const { t } = useTranslation(); // derived values - const currentTabEmptyState = ENotificationTab.ALL - ? EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE - : EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE; + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/search/notification" }); - return ; + return ( + <> + {ENotificationTab.ALL ? ( + + ) : ( + + )} + + ); }); diff --git a/web/core/components/workspace/sidebar/dropdown.tsx b/web/core/components/workspace/sidebar/dropdown.tsx index 93eb92bc08e..901dfcd65af 100644 --- a/web/core/components/workspace/sidebar/dropdown.tsx +++ b/web/core/components/workspace/sidebar/dropdown.tsx @@ -41,7 +41,7 @@ export const SidebarDropdown = observer(() => { }, { key: "settings", - name: t("workspace_settings"), + name: t("workspace_settings.label"), href: `/${workspaceSlug}/settings`, icon: Settings, access: [EUserPermissions.ADMIN], diff --git a/web/core/components/workspace/sidebar/user-menu.tsx b/web/core/components/workspace/sidebar/user-menu.tsx index e412fc7b65a..89e9cff3258 100644 --- a/web/core/components/workspace/sidebar/user-menu.tsx +++ b/web/core/components/workspace/sidebar/user-menu.tsx @@ -37,7 +37,7 @@ export const SidebarUserMenu = observer(() => { }, { key: "notifications", - labelTranslationKey: "inbox", + labelTranslationKey: "inbox.label", href: `/${workspaceSlug.toString()}/notifications/`, access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST], Icon: Inbox, diff --git a/web/core/constants/empty-state.tsx b/web/core/constants/empty-state.tsx deleted file mode 100644 index b502035de7f..00000000000 --- a/web/core/constants/empty-state.tsx +++ /dev/null @@ -1,960 +0,0 @@ -import { EUserPermissions } from "ee/constants/user-permissions"; -import { Plus, Shapes } from "lucide-react"; - -export interface EmptyStateDetails { - key: EmptyStateType; - title?: string; - description?: string; - path?: string; - primaryButton?: { - icon?: React.ReactNode; - text: string; - comicBox?: { - title?: string; - description?: string; - }; - }; - secondaryButton?: { - icon?: React.ReactNode; - text: string; - comicBox?: { - title?: string; - description?: string; - }; - }; - accessType?: "workspace" | "project"; - access?: any; -} - -export enum EmptyStateType { - WORKSPACE_DASHBOARD = "workspace-dashboard", - WORKSPACE_ANALYTICS = "workspace-analytics", - WORKSPACE_PROJECTS = "workspace-projects", - WORKSPACE_TEAMS = "workspace-teams", - WORKSPACE_INITIATIVES = "workspace-initiatives", - WORKSPACE_INITIATIVES_EMPTY_SEARCH = "workspace-initiatives-empty-search", - WORKSPACE_ALL_ISSUES = "workspace-all-issues", - WORKSPACE_ASSIGNED = "workspace-assigned", - WORKSPACE_CREATED = "workspace-created", - WORKSPACE_SUBSCRIBED = "workspace-subscribed", - WORKSPACE_CUSTOM_VIEW = "workspace-custom-view", - WORKSPACE_NO_PROJECTS = "workspace-no-projects", - WORKSPACE_PROJECT_NOT_FOUND = "workspace-project-not-found", - WORKSPACE_SETTINGS_API_TOKENS = "workspace-settings-api-tokens", - WORKSPACE_SETTINGS_WEBHOOKS = "workspace-settings-webhooks", - WORKSPACE_SETTINGS_EXPORT = "workspace-settings-export", - WORKSPACE_SETTINGS_IMPORT = "workspace-settings-import", - PROFILE_ACTIVITY = "profile-activity", - PROFILE_ASSIGNED = "profile-assigned", - PROFILE_CREATED = "profile-created", - PROFILE_SUBSCRIBED = "profile-subscribed", - PROJECT_SETTINGS_LABELS = "project-settings-labels", - PROJECT_SETTINGS_INTEGRATIONS = "project-settings-integrations", - PROJECT_SETTINGS_ESTIMATE = "project-settings-estimate", - PROJECT_CYCLES = "project-cycles", - PROJECT_CYCLE_NO_ISSUES = "project-cycle-no-issues", - PROJECT_CYCLE_ACTIVE = "project-cycle-active", - PROJECT_CYCLE_ALL = "project-cycle-all", - PROJECT_CYCLE_COMPLETED_NO_ISSUES = "project-cycle-completed-no-issues", - PROJECT_ARCHIVED_NO_CYCLES = "project-archived-no-cycles", - PROJECT_EMPTY_FILTER = "project-empty-filter", - PROJECT_ARCHIVED_EMPTY_FILTER = "project-archived-empty-filter", - PROJECT_DRAFT_EMPTY_FILTER = "project-draft-empty-filter", - PROJECT_NO_ISSUES = "project-no-issues", - PROJECT_ARCHIVED_NO_ISSUES = "project-archived-no-issues", - PROJECT_DRAFT_NO_ISSUES = "project-draft-no-issues", - VIEWS_EMPTY_SEARCH = "views-empty-search", - PROJECTS_EMPTY_SEARCH = "projects-empty-search", - MEMBERS_EMPTY_SEARCH = "members-empty-search", - PROJECT_MODULE_ISSUES = "project-module-issues", - PROJECT_MODULE = "project-module", - PROJECT_ARCHIVED_NO_MODULES = "project-archived-no-modules", - PROJECT_VIEW = "project-view", - PROJECT_PAGE = "project-page", - PROJECT_PAGE_PRIVATE = "project-page-private", - PROJECT_PAGE_PUBLIC = "project-page-public", - PROJECT_PAGE_ARCHIVED = "project-page-archived", - WORKSPACE_PAGE = "workspace-page", - WORKSPACE_PAGE_PRIVATE = "workspace-page-private", - WORKSPACE_PAGE_PUBLIC = "workspace-page-public", - WORKSPACE_PAGE_ARCHIVED = "workspace-page-archived", - - COMMAND_K_SEARCH_EMPTY_STATE = "command-k-search-empty-state", - ISSUE_RELATION_SEARCH_EMPTY_STATE = "issue-relation-search-empty-state", - ISSUE_RELATION_EMPTY_STATE = "issue-relation-empty-state", - ISSUE_COMMENT_EMPTY_STATE = "issue-comment-empty-state", - - EPIC_RELATION_SEARCH_EMPTY_STATE = "epic-relation-search-empty-state", - EPIC_RELATION_EMPTY_STATE = "epic-relation-empty-state", - - NOTIFICATION_DETAIL_EMPTY_STATE = "notification-detail-empty-state", - NOTIFICATION_ALL_EMPTY_STATE = "notification-all-empty-state", - NOTIFICATION_MENTIONS_EMPTY_STATE = "notification-mentions-empty-state", - NOTIFICATION_MY_ISSUE_EMPTY_STATE = "notification-my-issues-empty-state", - NOTIFICATION_CREATED_EMPTY_STATE = "notification-created-empty-state", - NOTIFICATION_SUBSCRIBED_EMPTY_STATE = "notification-subscribed-empty-state", - NOTIFICATION_ARCHIVED_EMPTY_STATE = "notification-archived-empty-state", - NOTIFICATION_SNOOZED_EMPTY_STATE = "notification-snoozed-empty-state", - NOTIFICATION_UNREAD_EMPTY_STATE = "notification-unread-empty-state", - - ACTIVE_CYCLE_PROGRESS_EMPTY_STATE = "active-cycle-progress-empty-state", - ACTIVE_CYCLE_CHART_EMPTY_STATE = "active-cycle-chart-empty-state", - ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE = "active-cycle-priority-issue-empty-state", - ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE = "active-cycle-assignee-empty-state", - ACTIVE_CYCLE_LABEL_EMPTY_STATE = "active-cycle-label-empty-state", - - WORKSPACE_ACTIVE_CYCLES = "workspace-active-cycles", - DISABLED_PROJECT_INBOX = "disabled-project-inbox", - DISABLED_PROJECT_CYCLE = "disabled-project-cycle", - DISABLED_PROJECT_MODULE = "disabled-project-module", - DISABLED_PROJECT_VIEW = "disabled-project-view", - DISABLED_PROJECT_PAGE = "disabled-project-page", - - INBOX_SIDEBAR_OPEN_TAB = "inbox-sidebar-open-tab", - INBOX_SIDEBAR_CLOSED_TAB = "inbox-sidebar-closed-tab", - INBOX_SIDEBAR_FILTER_EMPTY_STATE = "inbox-sidebar-filter-empty-state", - INBOX_DETAIL_EMPTY_STATE = "inbox-detail-empty-state", - - WORKSPACE_DRAFT_ISSUES = "workspace-draft-issues", - - PROJECT_NO_EPICS = "project-no-epics", - // Teams - TEAM_NO_ISSUES = "team-no-issues", - TEAM_EMPTY_FILTER = "team-empty-filter", - TEAM_VIEW = "team-view", - TEAM_PAGE = "team-page", - // stickies - STICKIES = "stickies", - STICKIES_SEARCH = "stickies-search", - // home widgets - HOME_WIDGETS = "home-widgets", -} - -const emptyStateDetails: Record = { - // workspace - [EmptyStateType.WORKSPACE_DASHBOARD]: { - key: EmptyStateType.WORKSPACE_DASHBOARD, - title: "Overview of your projects, activity, and metrics", - description: - " Welcome to Plane, we are excited to have you here. Create your first project and track your issues, and this page will transform into a space that helps you progress. Admins will also see items which help their team progress.", - path: "/empty-state/onboarding/dashboard", - // path: "/empty-state/onboarding/", - primaryButton: { - text: "Build your first project", - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_ANALYTICS]: { - key: EmptyStateType.WORKSPACE_ANALYTICS, - title: "Track progress, workloads, and allocations. Spot trends, remove blockers, and move work faster", - description: - "See scope versus demand, estimates, and scope creep. Get performance by team members and teams, and make sure your project runs on time.", - path: "/empty-state/onboarding/analytics", - primaryButton: { - text: "Start your first project", - comicBox: { - title: "Analytics works best with Cycles + Modules", - description: - "First, timebox your issues into Cycles and, if you can, group issues that span more than a cycle into Modules. Check out both on the left nav.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_PROJECTS]: { - key: EmptyStateType.WORKSPACE_PROJECTS, - title: "No active projects", - description: - "Think of each project as the parent for goal-oriented work. Projects are where Jobs, Cycles, and Modules live and, along with your colleagues, help you achieve that goal. Create a new project or filter for archived projects.", - path: "/empty-state/onboarding/projects", - primaryButton: { - text: "Start your first project", - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_TEAMS]: { - key: EmptyStateType.WORKSPACE_TEAMS, - title: "Teams", - description: "Teams are groups of people who collaborate on projects. Create a team to get started.", - path: "/empty-state/teams/teams", - primaryButton: { - text: "Create new team", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN], - }, - [EmptyStateType.WORKSPACE_INITIATIVES]: { - key: EmptyStateType.WORKSPACE_INITIATIVES, - title: "Organize work at the highest level with Initiatives", - description: - "When you need to organize work spanning several projects and teams, Initiatives come in handy. Connect projects and epics to initiatives, see automatically rolled up updates, and see the forests before you get to the trees.", - path: "/empty-state/initiatives/initiatives", - primaryButton: { - text: "Create an initiative", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_INITIATIVES_EMPTY_SEARCH]: { - key: EmptyStateType.WORKSPACE_INITIATIVES_EMPTY_SEARCH, - title: "No matching initiatives", - description: "No initiatives detected with the matching criteria. \n Create a new initiative instead.", - path: "/empty-state/search/project", - }, - // all-issues - [EmptyStateType.WORKSPACE_ALL_ISSUES]: { - key: EmptyStateType.WORKSPACE_ALL_ISSUES, - title: "No issues in the project", - description: "First project done! Now, slice your work into trackable pieces with issues. Let's go!", - path: "/empty-state/all-issues/all-issues", - primaryButton: { - text: "Create new issue", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_ASSIGNED]: { - key: EmptyStateType.WORKSPACE_ASSIGNED, - title: "No issues yet", - description: "Issues assigned to you can be tracked from here.", - path: "/empty-state/all-issues/assigned", - primaryButton: { - text: "Create new issue", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_CREATED]: { - key: EmptyStateType.WORKSPACE_CREATED, - title: "No issues yet", - description: "All issues created by you come here, track them here directly.", - path: "/empty-state/all-issues/created", - primaryButton: { - text: "Create new issue", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_SUBSCRIBED]: { - key: EmptyStateType.WORKSPACE_SUBSCRIBED, - title: "No issues yet", - description: "Subscribe to issues you are interested in, track all of them here.", - path: "/empty-state/all-issues/subscribed", - }, - [EmptyStateType.WORKSPACE_CUSTOM_VIEW]: { - key: EmptyStateType.WORKSPACE_CUSTOM_VIEW, - title: "No issues yet", - description: "Issues that applies to the filters, track all of them here.", - path: "/empty-state/all-issues/custom-view", - }, - [EmptyStateType.WORKSPACE_PROJECT_NOT_FOUND]: { - key: EmptyStateType.WORKSPACE_PROJECT_NOT_FOUND, - title: "No such project exists", - description: "To create issues or manage your work, you need to create a project or be a part of one.", - path: "/empty-state/onboarding/projects", - primaryButton: { - text: "Create Project", - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_NO_PROJECTS]: { - key: EmptyStateType.WORKSPACE_NO_PROJECTS, - title: "No project", - description: "To create issues or manage your work, you need to create a project or be a part of one.", - path: "/empty-state/onboarding/projects", - primaryButton: { - text: "Start your first project", - comicBox: { - title: "Everything starts with a project in Plane", - description: "A project could be a product’s roadmap, a marketing campaign, or launching a new car.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - // workspace settings - [EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS]: { - key: EmptyStateType.WORKSPACE_SETTINGS_API_TOKENS, - title: "No API tokens created", - description: - "Plane APIs can be used to integrate your data in Plane with any external system. Create a token to get started.", - path: "/empty-state/workspace-settings/api-tokens", - }, - [EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS]: { - key: EmptyStateType.WORKSPACE_SETTINGS_WEBHOOKS, - title: "No webhooks added", - description: "Create webhooks to receive real-time updates and automate actions.", - path: "/empty-state/workspace-settings/webhooks", - }, - [EmptyStateType.WORKSPACE_SETTINGS_EXPORT]: { - key: EmptyStateType.WORKSPACE_SETTINGS_EXPORT, - title: "No previous exports yet", - description: "Anytime you export, you will also have a copy here for reference.", - path: "/empty-state/workspace-settings/exports", - }, - [EmptyStateType.WORKSPACE_SETTINGS_IMPORT]: { - key: EmptyStateType.WORKSPACE_SETTINGS_IMPORT, - title: "No previous imports yet", - description: "Find all your previous imports here and download them.", - path: "/empty-state/workspace-settings/imports", - }, - // profile - [EmptyStateType.PROFILE_ACTIVITY]: { - key: EmptyStateType.PROFILE_ASSIGNED, - title: "No activities yet", - description: - "Get started by creating a new issue! Add details and properties to it. Explore more in Plane to see your activity.", - path: "/empty-state/profile/activity", - }, - [EmptyStateType.PROFILE_ASSIGNED]: { - key: EmptyStateType.PROFILE_ASSIGNED, - title: "No issues are assigned to you", - description: "Issues assigned to you can be tracked from here.", - path: "/empty-state/profile/assigned", - }, - [EmptyStateType.PROFILE_CREATED]: { - key: EmptyStateType.PROFILE_CREATED, - title: "No issues yet", - description: "All issues created by you come here, track them here directly.", - path: "/empty-state/profile/created", - }, - [EmptyStateType.PROFILE_SUBSCRIBED]: { - key: EmptyStateType.PROFILE_SUBSCRIBED, - title: "No issues yet", - description: "Subscribe to issues you are interested in, track all of them here.", - path: "/empty-state/profile/subscribed", - }, - // project settings - [EmptyStateType.PROJECT_SETTINGS_LABELS]: { - key: EmptyStateType.PROJECT_SETTINGS_LABELS, - title: "No labels yet", - description: "Create labels to help organize and filter issues in you project.", - path: "/empty-state/project-settings/labels", - }, - [EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS]: { - key: EmptyStateType.PROJECT_SETTINGS_INTEGRATIONS, - title: "No integrations configured", - description: "Configure GitHub and other integrations to sync your project issues.", - path: "/empty-state/project-settings/integrations", - }, - [EmptyStateType.PROJECT_SETTINGS_ESTIMATE]: { - key: EmptyStateType.PROJECT_SETTINGS_ESTIMATE, - title: "No estimates added", - description: "Create a set of estimates to communicate the amount of work per issue.", - path: "/empty-state/project-settings/estimates", - }, - // project cycles - [EmptyStateType.PROJECT_CYCLES]: { - key: EmptyStateType.PROJECT_CYCLES, - title: "Group and timebox your work in Cycles.", - description: - "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team.", - path: "/empty-state/onboarding/cycles", - primaryButton: { - text: "Set your first cycle", - comicBox: { - title: "Cycles are repetitive time-boxes.", - description: - "A sprint, an iteration, and or any other term you use for weekly or fortnightly tracking of work is a cycle.", - }, - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_CYCLE_NO_ISSUES]: { - key: EmptyStateType.PROJECT_CYCLE_NO_ISSUES, - title: "No issues added to the cycle", - description: "Add or create issues you wish to timebox and deliver within this cycle", - path: "/empty-state/cycle-issues/", - primaryButton: { - text: "Create new issue ", - }, - secondaryButton: { - text: "Add an existing issue", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_CYCLE_ACTIVE]: { - key: EmptyStateType.PROJECT_CYCLE_ACTIVE, - title: "No active cycle", - description: - "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", - path: "/empty-state/cycle/active", - }, - [EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES]: { - key: EmptyStateType.PROJECT_CYCLE_COMPLETED_NO_ISSUES, - title: "No issues in the cycle", - description: - "No issues in the cycle. Issues are either transferred or hidden. To see hidden issues if any, update your display properties accordingly.", - path: "/empty-state/cycle/completed-no-issues", - }, - [EmptyStateType.PROJECT_ARCHIVED_NO_CYCLES]: { - key: EmptyStateType.PROJECT_ARCHIVED_NO_CYCLES, - title: "No archived cycles yet", - description: "To tidy up your project, archive completed cycles. Find them here once archived.", - path: "/empty-state/archived/empty-cycles", - }, - [EmptyStateType.PROJECT_CYCLE_ALL]: { - key: EmptyStateType.PROJECT_CYCLE_ALL, - title: "No cycles", - description: - "An active cycle includes any period that encompasses today's date within its range. Find the progress and details of the active cycle here.", - path: "/empty-state/cycle/active", - }, - // empty filters - [EmptyStateType.PROJECT_EMPTY_FILTER]: { - key: EmptyStateType.PROJECT_EMPTY_FILTER, - title: "No issues found matching the filters applied", - path: "/empty-state/empty-filters/", - secondaryButton: { - text: "Clear all filters", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER]: { - key: EmptyStateType.PROJECT_ARCHIVED_EMPTY_FILTER, - title: "No issues found matching the filters applied", - path: "/empty-state/empty-filters/", - secondaryButton: { - text: "Clear all filters", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER]: { - key: EmptyStateType.PROJECT_DRAFT_EMPTY_FILTER, - title: "No issues found matching the filters applied", - path: "/empty-state/empty-filters/", - secondaryButton: { - text: "Clear all filters", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - // project issues - [EmptyStateType.PROJECT_NO_ISSUES]: { - key: EmptyStateType.PROJECT_NO_ISSUES, - title: "Create an issue and assign it to someone, even yourself", - description: - "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", - path: "/empty-state/onboarding/issues", - primaryButton: { - text: "Create your first issue", - comicBox: { - title: "Issues are building blocks in Plane.", - description: - "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", - }, - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES]: { - key: EmptyStateType.PROJECT_ARCHIVED_NO_ISSUES, - title: "No archived issues yet", - description: - "Manually or through automation, you can archive issues that are completed or cancelled. Find them here once archived.", - path: "/empty-state/archived/empty-issues", - primaryButton: { - text: "Set automation", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_DRAFT_NO_ISSUES]: { - key: EmptyStateType.PROJECT_DRAFT_NO_ISSUES, - title: "No draft issues yet", - description: - "Quickly stepping away but want to keep your place? No worries – save a draft now. Your issues will be right here waiting for you.", - path: "/empty-state/draft/draft-issues-empty", - }, - [EmptyStateType.VIEWS_EMPTY_SEARCH]: { - key: EmptyStateType.VIEWS_EMPTY_SEARCH, - title: "No matching views", - description: "No views match the search criteria. \n Create a new view instead.", - path: "/empty-state/search/views", - }, - [EmptyStateType.PROJECTS_EMPTY_SEARCH]: { - key: EmptyStateType.PROJECTS_EMPTY_SEARCH, - title: "No matching projects", - description: "No projects detected with the matching criteria. Create a new project instead.", - path: "/empty-state/search/project", - }, - [EmptyStateType.MEMBERS_EMPTY_SEARCH]: { - key: EmptyStateType.MEMBERS_EMPTY_SEARCH, - title: "No matching members", - description: "Add them to the project if they are already a part of the workspace", - path: "/empty-state/search/member", - }, - // project module - [EmptyStateType.PROJECT_MODULE_ISSUES]: { - key: EmptyStateType.PROJECT_MODULE_ISSUES, - title: "No issues in the module", - description: "Create or add issues which you want to accomplish as part of this module", - path: "/empty-state/module-issues/", - primaryButton: { - text: "Create new issue ", - }, - secondaryButton: { - text: "Add an existing issue", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_MODULE]: { - key: EmptyStateType.PROJECT_MODULE, - title: "Map your project milestones to Modules and track aggregated work easily.", - description: - "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. They have their own periods and deadlines as well as analytics to help you see how close or far you are from a milestone.", - path: "/empty-state/onboarding/modules", - primaryButton: { - text: "Build your first module", - comicBox: { - title: "Modules help group work by hierarchy.", - description: "A cart module, a chassis module, and a warehouse module are all good example of this grouping.", - }, - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_ARCHIVED_NO_MODULES]: { - key: EmptyStateType.PROJECT_ARCHIVED_NO_MODULES, - title: "No archived Modules yet", - description: "To tidy up your project, archive completed or cancelled modules. Find them here once archived.", - path: "/empty-state/archived/empty-modules", - }, - // project views - [EmptyStateType.PROJECT_VIEW]: { - key: EmptyStateType.PROJECT_VIEW, - title: "Save filtered views for your project. Create as many as you need", - description: - "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best.", - path: "/empty-state/onboarding/views", - primaryButton: { - text: "Create your first view", - comicBox: { - title: "Views work atop Issue properties.", - description: "You can create a view from here with as many properties as filters as you see fit.", - }, - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - }, - // project pages - [EmptyStateType.PROJECT_PAGE]: { - key: EmptyStateType.PROJECT_PAGE, - title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", - description: - "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", - path: "/empty-state/onboarding/pages", - primaryButton: { - text: "Create your first page", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_PAGE_PRIVATE]: { - key: EmptyStateType.PROJECT_PAGE_PRIVATE, - title: "No private pages yet", - description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", - path: "/empty-state/pages/private", - primaryButton: { - text: "Create your first page", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_PAGE_PUBLIC]: { - key: EmptyStateType.PROJECT_PAGE_PUBLIC, - title: "No public pages yet", - description: "See pages shared with everyone in your project right here.", - path: "/empty-state/pages/public", - primaryButton: { - text: "Create your first page", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_PAGE_ARCHIVED]: { - key: EmptyStateType.PROJECT_PAGE_ARCHIVED, - title: "No archived pages yet", - description: "Archive pages not on your radar. Access them here when needed.", - path: "/empty-state/pages/archived", - }, - [EmptyStateType.WORKSPACE_PAGE]: { - key: EmptyStateType.WORKSPACE_PAGE, - title: "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started", - description: - "Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", - path: "/empty-state/onboarding/pages", - primaryButton: { - text: "Create your first page", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_PAGE_PRIVATE]: { - key: EmptyStateType.WORKSPACE_PAGE_PRIVATE, - title: "No private pages yet", - description: "Keep your private thoughts here. When you're ready to share, the team's just a click away.", - path: "/empty-state/pages/private", - primaryButton: { - text: "Create your first page", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_PAGE_PUBLIC]: { - key: EmptyStateType.WORKSPACE_PAGE_PUBLIC, - title: "No public pages yet", - description: "See pages shared with everyone in your workspace right here.", - path: "/empty-state/pages/public", - primaryButton: { - text: "Create your first page", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.WORKSPACE_PAGE_ARCHIVED]: { - key: EmptyStateType.WORKSPACE_PAGE_ARCHIVED, - title: "No archived pages yet", - description: "Archive pages not on your radar. Access them here when needed.", - path: "/empty-state/pages/archived", - }, - - [EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE]: { - key: EmptyStateType.COMMAND_K_SEARCH_EMPTY_STATE, - title: "No results found", - path: "/empty-state/search/search", - }, - [EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE]: { - key: EmptyStateType.ISSUE_RELATION_SEARCH_EMPTY_STATE, - title: "No matching issues found", - path: "/empty-state/search/search", - }, - [EmptyStateType.ISSUE_RELATION_EMPTY_STATE]: { - key: EmptyStateType.ISSUE_RELATION_EMPTY_STATE, - title: "No issues found", - path: "/empty-state/search/issues", - }, - - [EmptyStateType.EPIC_RELATION_SEARCH_EMPTY_STATE]: { - key: EmptyStateType.EPIC_RELATION_SEARCH_EMPTY_STATE, - title: "No matching epics found", - path: "/empty-state/search/search", - }, - [EmptyStateType.EPIC_RELATION_EMPTY_STATE]: { - key: EmptyStateType.EPIC_RELATION_EMPTY_STATE, - title: "No epics found", - path: "/empty-state/search/issues", - }, - - [EmptyStateType.ISSUE_COMMENT_EMPTY_STATE]: { - key: EmptyStateType.ISSUE_COMMENT_EMPTY_STATE, - title: "No comments yet", - description: "Comments can be used as a discussion and \n follow-up space for the issues", - path: "/empty-state/search/comments", - }, - - [EmptyStateType.NOTIFICATION_DETAIL_EMPTY_STATE]: { - key: EmptyStateType.INBOX_DETAIL_EMPTY_STATE, - title: "Select to view details.", - path: "/empty-state/intake/issue-detail", - }, - [EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE, - title: "No issues assigned", - description: "Updates for issues assigned to you can be \n seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE, - title: "No issues assigned", - description: "Updates for issues assigned to you can be \n seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE, - title: "No issues assigned", - description: "Updates for issues assigned to you can be \n seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE, - title: "No updates to issues", - description: "Updates to issues created by you can be \n seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE, - title: "No updates to issues", - description: "Updates to any issue you are \n subscribed to can be seen here", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_UNREAD_EMPTY_STATE, - title: "No unread notifications", - description: "Congratulations, you are up-to-date \n with everything happening in the issues \n you care about", - path: "/empty-state/search/notification", - }, - [EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE, - title: "No snoozed notifications yet", - description: "Any notification you snooze for later will \n be available here to act upon", - path: "/empty-state/search/snooze", - }, - [EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE]: { - key: EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE, - title: "No archived notifications yet", - description: "Any notification you archive will be \n available here to help you focus", - path: "/empty-state/search/archive", - }, - - [EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE, - title: "Add issues to the cycle to view it's \n progress", - path: "/empty-state/active-cycle/progress", - }, - [EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_CHART_EMPTY_STATE, - title: "Add issues to the cycle to view the \n burndown chart.", - path: "/empty-state/active-cycle/chart", - }, - [EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_PRIORITY_ISSUE_EMPTY_STATE, - title: "Observe high priority issues tackled in \n the cycle at a glance.", - path: "/empty-state/active-cycle/priority", - }, - [EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_ASSIGNEE_EMPTY_STATE, - title: "Add assignees to issues to see a \n breakdown of work by assignees.", - path: "/empty-state/active-cycle/assignee", - }, - [EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE]: { - key: EmptyStateType.ACTIVE_CYCLE_LABEL_EMPTY_STATE, - title: "Add labels to issues to see the \n breakdown of work by labels.", - path: "/empty-state/active-cycle/label", - }, - [EmptyStateType.WORKSPACE_ACTIVE_CYCLES]: { - key: EmptyStateType.WORKSPACE_ACTIVE_CYCLES, - title: "No active cycles", - description: - "Cycles of your projects that includes any period that encompasses today's date within its range. Find the progress and details of all your active cycle here.", - path: "/empty-state/onboarding/workspace-active-cycles", - }, - [EmptyStateType.DISABLED_PROJECT_INBOX]: { - key: EmptyStateType.DISABLED_PROJECT_INBOX, - title: "Intake is not enabled for the project.", - description: - "Intake helps you manage incoming requests to your project and add them as issues in your workflow. Enable intake \n from project settings to manage requests.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/intake", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.DISABLED_PROJECT_CYCLE]: { - key: EmptyStateType.DISABLED_PROJECT_CYCLE, - title: "Cycles is not enabled for this project.", - description: - "Break work down by timeboxed chunks, work backwards from your project deadline to set dates, and make tangible progress as a team. Enable the cycles feature for your project to start using them.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/cycles", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.DISABLED_PROJECT_MODULE]: { - key: EmptyStateType.DISABLED_PROJECT_MODULE, - title: "Modules are not enabled for the project.", - description: - "A group of issues that belong to a logical, hierarchical parent form a module. Think of them as a way to track work by project milestones. Enable modules from project settings.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/modules", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.DISABLED_PROJECT_PAGE]: { - key: EmptyStateType.DISABLED_PROJECT_PAGE, - title: "Pages are not enabled for the project.", - description: - "Pages are thought spotting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. Enable the pages feature to start creating them in your project.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/pages", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.DISABLED_PROJECT_VIEW]: { - key: EmptyStateType.DISABLED_PROJECT_VIEW, - title: "Views is not enabled for this project.", - description: - "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a project can see everyone’s views and choose whichever suits their needs best. Enable views in the project settings to start using them.", - accessType: "project", - access: [EUserPermissions.ADMIN], - path: "/empty-state/disabled-feature/views", - primaryButton: { - text: "Manage features", - }, - }, - [EmptyStateType.INBOX_SIDEBAR_OPEN_TAB]: { - key: EmptyStateType.INBOX_SIDEBAR_OPEN_TAB, - title: "No open issues", - description: "Find open issues here. Create new issue.", - path: "/empty-state/intake/intake-issue", - }, - [EmptyStateType.INBOX_SIDEBAR_CLOSED_TAB]: { - key: EmptyStateType.INBOX_SIDEBAR_CLOSED_TAB, - title: "No closed issues", - description: "All the issues whether accepted or \n declined can be found here.", - path: "/empty-state/intake/intake-issue", - }, - [EmptyStateType.INBOX_SIDEBAR_FILTER_EMPTY_STATE]: { - key: EmptyStateType.INBOX_SIDEBAR_FILTER_EMPTY_STATE, - title: "No matching issues", - description: "No issue matches filter applied in intake. \n Create a new issue.", - path: "/empty-state/intake/filter-issue", - }, - [EmptyStateType.INBOX_DETAIL_EMPTY_STATE]: { - key: EmptyStateType.INBOX_DETAIL_EMPTY_STATE, - title: "Select an issue to view its details.", - path: "/empty-state/intake/issue-detail", - }, - [EmptyStateType.WORKSPACE_DRAFT_ISSUES]: { - key: EmptyStateType.WORKSPACE_DRAFT_ISSUES, - title: "Half-written issues, and soon, comments will show up here.", - description: "To try this out, start adding an issue and leave it mid-way or create your first draft below. 😉", - path: "/empty-state/workspace-draft/issue", - primaryButton: { - text: "Create your first draft", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.PROJECT_NO_EPICS]: { - key: EmptyStateType.PROJECT_NO_EPICS, - title: "Create an epic and assign it to someone, even yourself", - description: - "For larger bodies of work that span several cycles and can live across modules, create an epic. Link issues and sub-issues in a project to an epic and jump into an issue from the overview.", - path: "/empty-state/onboarding/issues", - primaryButton: { - text: "Create an Epic", - }, - accessType: "project", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - // Teams - [EmptyStateType.TEAM_NO_ISSUES]: { - key: EmptyStateType.TEAM_NO_ISSUES, - title: "Create an issue in your team projects and assign it to someone, even yourself", - description: - "Think of issues as jobs, tasks, work, or JTBD. Which we like. An issue and its sub-issues are usually time-based actionables assigned to members of your team. Your team creates, assigns, and completes issues to move your project towards its goal.", - path: "/empty-state/onboarding/issues", - primaryButton: { - text: "Create your first issue", - comicBox: { - title: "Issues are building blocks in Plane.", - description: - "Redesign the Plane UI, Rebrand the company, or Launch the new fuel injection system are examples of issues that likely have sub-issues.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.TEAM_EMPTY_FILTER]: { - key: EmptyStateType.TEAM_EMPTY_FILTER, - title: "No issues found matching the filters applied", - path: "/empty-state/empty-filters/", - secondaryButton: { - text: "Clear all filters", - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.TEAM_VIEW]: { - key: EmptyStateType.TEAM_VIEW, - title: "Save filtered views for your team. Create as many as you need", - description: - "Views are a set of saved filters that you use frequently or want easy access to. All your colleagues in a team can see everyone’s views and choose whichever suits their needs best.", - path: "/empty-state/onboarding/views", - primaryButton: { - text: "Create your first view", - comicBox: { - title: "Views work atop Issue properties.", - description: "You can create a view from here with as many properties as filters as you see fit.", - }, - }, - accessType: "workspace", - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER], - }, - [EmptyStateType.TEAM_PAGE]: { - key: EmptyStateType.TEAM_PAGE, - title: "Team pages are coming soon!", - description: - "Write a note, a doc, or a full knowledge base. Get Galileo, Plane’s AI assistant, to help you get started. Pages are thoughts potting space in Plane. Take down meeting notes, format them easily, embed issues, lay them out using a library of components, and keep them all in your project’s context. To make short work of any doc, invoke Galileo, Plane’s AI, with a shortcut or the click of a button.", - path: "/empty-state/onboarding/pages", - }, - [EmptyStateType.STICKIES]: { - key: EmptyStateType.STICKIES, - title: "Stickies are quick notes and to-dos you take down on the fly.", - description: - "Capture ideas, ahas, brainwaves, light-bulb moments without scrambling for a pen and paper, hunting for the recorder app on your phone, or firing up a notes app only to forget all about it later. Keep them all right next to your work so you can easily come back, build them up, or well, discard them.", - path: "/empty-state/stickies/stickies", - primaryButton: { - icon: , - text: "Add sticky", - }, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - accessType: "workspace", - }, - [EmptyStateType.STICKIES_SEARCH]: { - key: EmptyStateType.STICKIES_SEARCH, - title: "That doesn't match any of your stickies.", - description: "Try a different term or let us know\nif you are sure your search is right. ", - path: "/empty-state/stickies/stickies-search", - primaryButton: { - icon: , - text: "Add sticky", - }, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - accessType: "workspace", - }, - [EmptyStateType.HOME_WIDGETS]: { - key: EmptyStateType.HOME_WIDGETS, - title: "So much to add, yet such empty", - description: "You have turned off all your widgets. Turn some or\nall of them back on to see delightful things.", - path: "/empty-state/dashboard/widgets", - primaryButton: { - icon: , - text: "Manage widgets", - }, - access: [EUserPermissions.ADMIN, EUserPermissions.MEMBER, EUserPermissions.GUEST], - accessType: "workspace", - }, -} as const; - -export const EMPTY_STATE_DETAILS: Record = emptyStateDetails; diff --git a/web/core/constants/profile.ts b/web/core/constants/profile.ts index d6ccf658324..14fc08638de 100644 --- a/web/core/constants/profile.ts +++ b/web/core/constants/profile.ts @@ -10,7 +10,7 @@ export const PROFILE_ACTION_LINKS: { Icon: React.FC; }[] = [ { - key: "profile", + key: "profile.label", label: "Profile", href: `/profile`, highlight: (pathname: string) => pathname === "/profile/", diff --git a/web/core/hooks/use-resolved-asset-path.tsx b/web/core/hooks/use-resolved-asset-path.tsx index f5543d2006e..4233dc6469c 100644 --- a/web/core/hooks/use-resolved-asset-path.tsx +++ b/web/core/hooks/use-resolved-asset-path.tsx @@ -4,14 +4,23 @@ type AssetPathConfig = { basePath: string; additionalPath?: string; extension?: string; + includeThemeInPath?: boolean; }; -export const useResolvedAssetPath = ({ basePath, additionalPath = "", extension = "webp" }: AssetPathConfig) => { +export const useResolvedAssetPath = ({ + basePath, + additionalPath = "", + extension = "webp", + includeThemeInPath = true, +}: AssetPathConfig) => { // hooks const { resolvedTheme } = useTheme(); - // resolved theme const theme = resolvedTheme === "light" ? "light" : "dark"; + if (!includeThemeInPath) { + return `${additionalPath && additionalPath !== "" ? `${basePath}${additionalPath}` : basePath}.${extension}`; + } + return `${additionalPath && additionalPath !== "" ? `${basePath}${additionalPath}` : basePath}-${theme}.${extension}`; }; diff --git a/web/core/layouts/auth-layout/project-wrapper.tsx b/web/core/layouts/auth-layout/project-wrapper.tsx index dcba2a8fef2..61d8f89d4b3 100644 --- a/web/core/layouts/auth-layout/project-wrapper.tsx +++ b/web/core/layouts/auth-layout/project-wrapper.tsx @@ -4,14 +4,13 @@ import { FC, ReactNode, useEffect } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; import useSWR from "swr"; - +// plane imports +import { useTranslation } from "@plane/i18n"; // components import { JoinProject } from "@/components/auth-screens"; import { LogoSpinner } from "@/components/common"; -import { EmptyState } from "@/components/empty-state"; +import { ComicBoxButton, DetailedEmptyState } from "@/components/empty-state"; import { ETimeLineTypeType } from "@/components/gantt-chart/contexts"; -//constants -import { EmptyStateType } from "@/constants/empty-state"; // hooks import { useCommandPalette, @@ -26,6 +25,7 @@ import { useProjectView, useUserPermissions, } from "@/hooks/store"; +import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path"; import { useTimeLineChart } from "@/hooks/use-timeline-chart"; // local import { persistence } from "@/local-db/storage.sqlite"; @@ -41,6 +41,8 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { children, isLoading: isParentLoading = false } = props; // router const { workspaceSlug, projectId } = useParams(); + // plane hooks + const { t } = useTranslation(); // store hooks const { toggleCreateProjectModal } = useCommandPalette(); const { setTrackElement } = useEventTracker(); @@ -56,6 +58,10 @@ export const ProjectAuthWrapper: FC = observer((props) => { const { fetchProjectStates, fetchProjectStateTransitions } = useProjectState(); const { fetchProjectLabels } = useLabel(); const { getProjectEstimates } = useProjectEstimates(); + + // helper hooks + const resolvedPath = useResolvedAssetPath({ basePath: "/empty-state/onboarding/projects" }); + // derived values const projectExists = projectId ? getProjectById(projectId.toString()) : null; const projectMemberInfo = projectUserInfo?.[workspaceSlug?.toString()]?.[projectId?.toString()]; @@ -151,6 +157,12 @@ export const ProjectAuthWrapper: FC = observer((props) => { { revalidateIfStale: false, revalidateOnFocus: false } ); + // permissions + const canPerformEmptyStateActions = allowPermissions( + [EUserPermissions.ADMIN, EUserPermissions.MEMBER], + EUserPermissionsLevel.WORKSPACE + ); + // check if the project member apis is loading if (isParentLoading || (!projectMemberInfo && projectId && hasPermissionToCurrentProject === null)) return ( @@ -168,13 +180,22 @@ export const ProjectAuthWrapper: FC = observer((props) => { if (!loader && !projectExists && projectId && !!hasPermissionToCurrentProject === false) return (
    - { - setTrackElement("Projects page empty state"); - toggleCreateProjectModal(true); - }} + { + setTrackElement("Project empty state"); + toggleCreateProjectModal(true); + }} + disabled={!canPerformEmptyStateActions} + /> + } />
    ); diff --git a/web/core/store/user/permissions.store.ts b/web/core/store/user/permissions.store.ts index ba1211b0f1a..2c745e6bc2d 100644 --- a/web/core/store/user/permissions.store.ts +++ b/web/core/store/user/permissions.store.ts @@ -3,6 +3,7 @@ import unset from "lodash/unset"; import { action, makeObservable, observable, runInAction } from "mobx"; import { computedFn } from "mobx-utils"; // types +import { EUserProjectRoles, EUserWorkspaceRoles } from "@plane/constants"; import { IProjectMember, IUserProjectsRole, IWorkspaceMemberMe } from "@plane/types"; // plane web types import { @@ -22,6 +23,8 @@ import { CoreRootStore } from "@/store/root.store"; // derived services const workspaceService = new WorkspaceService(); +type ETempUserRole = TUserPermissions | EUserWorkspaceRoles | EUserProjectRoles; // TODO: Remove this once we have migrated user permissions to enums to plane constants package + export interface IUserPermissionStore { loader: boolean; // observables @@ -36,7 +39,7 @@ export interface IUserPermissionStore { projectId: string ) => TUserPermissions | undefined; allowPermissions: ( - allowPermissions: TUserPermissions[], + allowPermissions: ETempUserRole[], level: TUserPermissionsLevel, workspaceSlug?: string, projectId?: string, @@ -115,7 +118,7 @@ export class UserPermissionStore implements IUserPermissionStore { * @returns { boolean } */ allowPermissions = ( - allowPermissions: TUserPermissions[], + allowPermissions: ETempUserRole[], level: TUserPermissionsLevel, workspaceSlug?: string, projectId?: string, @@ -140,7 +143,7 @@ export class UserPermissionStore implements IUserPermissionStore { this.projectPermissionsByWorkspaceSlugAndProjectId(workspaceSlug, projectId)) as EUserPermissions | undefined; } - if (currentUserRole && allowPermissions.includes(currentUserRole)) { + if (currentUserRole && allowPermissions.includes(currentUserRole as TUserPermissions)) { if (onPermissionAllowed) { return onPermissionAllowed(); } else {