Compare commits

...

1 Commits

Author SHA1 Message Date
Johannes 2d6abf484d chore: finalize api keys, translations, and docs updates
Apply remaining admin API key UI tweaks, add matching locale copy updates, and include supporting development documentation for the question-bank workflow.

Made-with: Cursor
2026-04-24 10:41:49 +02:00
3 changed files with 253 additions and 75 deletions
+74 -17
View File
@@ -328,6 +328,7 @@
"not_authenticated": "You are not authenticated to perform this action.",
"not_authorized": "Not authorized",
"not_connected": "Not Connected",
"not_set": "Not set",
"note": "Note",
"notifications": "Notifications",
"number": "Number",
@@ -366,7 +367,7 @@
"please_upgrade_your_plan": "Please upgrade your plan",
"powered_by_formbricks": "Powered by Formbricks",
"preview": "Preview",
"preview_survey": "Preview Survey",
"preview_survey": "Preview survey",
"privacy": "Privacy Policy",
"product_manager": "Product Manager",
"production": "Production",
@@ -389,6 +390,7 @@
"report_survey": "Report Survey",
"request_trial_license": "Request trial license",
"reset_to_default": "Reset to default",
"resize": "Resize",
"response": "Response",
"response_id": "Response ID",
"responses": "Responses",
@@ -427,6 +429,7 @@
"some_files_failed_to_upload": "Some files failed to upload",
"something_went_wrong": "Something went wrong",
"something_went_wrong_please_try_again": "Something went wrong. Please try again.",
"soon": "Soon",
"sort_by": "Sort by",
"start_free_trial": "Start free trial",
"status": "Status",
@@ -1762,9 +1765,11 @@
"please_select_dashboard": "Please select a dashboard",
"predefined_measures": "Predefined Measures",
"preset": "Preset",
"preview_chart": "Preview chart",
"query_executed_successfully": "Query executed successfully",
"reset_to_ai_suggestion": "Reset to AI suggestion",
"save_chart": "Save Chart",
"save_and_add_to_dashboard": "Save & add to dashboard",
"save_chart": "Save chart",
"save_chart_dialog_title": "Save Chart",
"select_data_source": "Select a data source",
"select_data_source_first": "Please select a data source first",
@@ -1776,7 +1781,8 @@
"start_date": "Start date",
"time_dimension": "Time Dimension",
"time_dimension_title": "Add time-based grouping",
"time_dimension_toggle_description": "Monitor trends over time."
"time_dimension_toggle_description": "Monitor trends over time.",
"update_chart": "Update chart"
},
"dashboards": {
"add_count_charts": "Add {count} chart(s)",
@@ -1807,7 +1813,9 @@
"no_dashboards_found": "No dashboards found.",
"no_data_message": "No Data. There is currently no information to display. Add charts to build your dashboard.",
"please_enter_name": "Please enter a dashboard name"
}
},
"no_feedback_records_message": "You don't have Feedback Records to report on. Setup Feedback Sources to feed data into the system.",
"setup_feedback_source": "Setup feedback sources"
},
"api_keys": {
"add_api_key": "Add API Key",
@@ -1820,7 +1828,7 @@
"api_key_updated": "API Key updated",
"delete_api_key_confirmation": "Any applications using this key will no longer be able to access your Formbricks data.",
"duplicate_access": "Duplicate workspace access not allowed",
"no_api_keys_yet": "You do not have any API keys yet",
"no_api_keys_yet": "No API keys found. Create an API key to get started.",
"no_env_permissions_found": "No environment permissions found",
"organization_access": "Organization Access",
"organization_access_description": "Select read or write privileges for organization-wide resources.",
@@ -2506,14 +2514,14 @@
"archive_directory": "Archive Directory",
"archive_not_allowed": "You are not allowed to archive this directory.",
"are_you_sure_you_want_to_archive": "Are you sure you want to archive this directory? Workspaces will no longer have access to it.",
"assign_workspaces_description": "Control which workspaces can access this feedback record directory.",
"assign_workspaces_description": "Control which workspaces can access this directory. Each workspace can only access one directory.",
"connectors_description": "Connectors that send feedback records to this directory.",
"create_feedback_directory": "Create feedback directory",
"description": "Manage feedback record directories and their workspace assignments.",
"directory_archived_successfully": "Directory archived successfully",
"directory_created_successfully": "Directory created successfully",
"directory_id": "Directory ID",
"directory_name": "Directory Name",
"directory_name": "Directory name",
"directory_settings_description": "Manage directory name, workspace assignments, and more.",
"directory_settings_title": "{directoryName} Settings",
"directory_unarchived_successfully": "Directory unarchived successfully",
@@ -2523,13 +2531,19 @@
"error_directory_name_duplicate": "A feedback record directory with this name already exists.",
"error_directory_name_required": "Directory name is required.",
"error_directory_workspaces_invalid_org": "Some specified workspaces do not belong to this organization.",
"has_access": "Has access",
"nav_label": "Feedback Directories",
"no_access": "You do not have permission to manage feedback record directories.",
"no_connectors": "No connectors linked to this directory yet.",
"pause_connectors_confirmation_description": "{count, plural, one {1 connector will be paused because its workspace no longer has access to this directory. Continue?} other {{count} connectors will be paused because their workspaces no longer have access to this directory. Continue?}}",
"pause_connectors_confirmation_title": "Pause affected connectors?",
"select_workspaces_placeholder": "Select workspaces...",
"show_archived": "Show archived",
"title": "Feedback Record Directories",
"unarchive": "Unarchive"
"unarchive": "Unarchive",
"unarchive_workspace_conflict": "Cannot unarchive this directory because one or more workspaces are already assigned to another Feedback Directory.",
"workspace_access": "Workspace access",
"workspace_already_assigned_to_directory": "Already assigned to \"{directoryName}\""
},
"general": {
"ai_data_analysis_disabled_for_organization": "AI data analysis is disabled for this organization.",
@@ -3551,7 +3565,7 @@
"add_tag": "Add Tag",
"count": "Count",
"delete_tag_confirmation": "Are you sure you want to delete this tag?",
"manage_tags": "Manage Tags",
"manage_tags": "Manage tags",
"manage_tags_description": "Merge and remove response tags.",
"merge": "Merge",
"no_tag_found": "No tag found",
@@ -3570,15 +3584,21 @@
"team_settings_description": "See which teams can access this workspace."
},
"unify": {
"add_feedback_record": "Add feedback record",
"add_feedback_record_description": "Create a feedback record manually.",
"add_feedback_source": "Add Feedback Source",
"add_source": "Add source",
"allowed_values": "Allowed values: {values}",
"api_ingestion": "API ingestion",
"api_ingestion_manage_api_keys": "Manage API keys",
"api_ingestion_settings_description": "Send feedback records directly to Formbricks via HTTP.",
"auto_generated": "Auto-generated",
"change_file": "Change file",
"click_load_sample_csv": "Click 'Load sample CSV' to see columns",
"click_to_upload": "Click to upload",
"collected_at": "Collected At",
"configure_import": "Configure import",
"configure_mapping": "Configure Mapping",
"configure_mapping": "Configure mapping",
"connection": "Connection",
"connector_created_successfully": "Connector created successfully",
"connector_deleted_successfully": "Connector deleted successfully",
@@ -3593,14 +3613,18 @@
"csv_empty_column_headers": "CSV contains empty column headers. All columns must have a name.",
"csv_file_too_large": "CSV file is too large. Maximum size is 2MB.",
"csv_files_only": "CSV files only",
"csv_import": "CSV Import",
"csv_import": "CSV import",
"csv_import_complete": "CSV import complete: {successes} succeeded, {failures} failed, {skipped} skipped",
"csv_import_duplicate_warning": "Importing data twice will create duplicate records.",
"csv_inconsistent_columns": "Row {row} has inconsistent columns. All rows must have the same headers.",
"csv_max_records": "Maximum {max} records allowed.",
"custom_source_type": "Custom source type",
"custom_source_type_placeholder": "Enter custom source type",
"default_connector_name_csv": "CSV Import",
"default_connector_name_formbricks": "Formbricks Survey Connection",
"deselect_all": "Deselect all",
"discard_feedback_record_changes_description": "Your changes will be lost if you close this drawer.",
"discard_feedback_record_changes_title": "Discard unsaved changes?",
"drop_a_field_here": "Drop a field here",
"drop_field_or": "Drop field or",
"edit_csv_mapping": "Edit CSV mapping",
@@ -3610,24 +3634,46 @@
"enum": "enum",
"failed_to_load_feedback_records": "Failed to load feedback records",
"feedback_date": "Current date",
"feedback_record_created_successfully": "Feedback record created successfully",
"feedback_record_details": "Feedback record details",
"feedback_record_details_description": "Review and update feedback record fields.",
"feedback_record_directory": "Feedback Record Directory",
"feedback_record_fields": "Feedback Record Fields",
"feedback_record_mcp": "Feedback Record MCP",
"feedback_record_updated_successfully": "Feedback record updated successfully",
"feedback_record_value_required": "A value is required for the selected field type",
"feedback_records": "Feedback Records",
"feedback_records_refreshed": "Feedback records refreshed",
"feedback_sources": "Feedback Sources",
"feedback_sources_directory_access_multiple": "This workspace has access to the {directoryNames} feedback directories.",
"feedback_sources_directory_access_single": "This workspace has access to the {directoryNames} feedback directory.",
"feedback_sources_settings_description": "Connect and manage the sources that feed your feedback records.",
"field_group_id": "Field Group ID",
"field_group_label": "Field Group Label",
"field_id": "Field ID",
"field_label": "Field Label",
"field_type": "Field Type",
"formbricks_surveys": "Formbricks Surveys",
"frd_cannot_be_changed": "Feedback directory cannot be changed after creation.",
"formbricks_surveys": "Formbricks survey",
"go_to_feedback_record_directories": "Go to directories settings",
"historical_import_complete": "Import complete: {successes} succeeded, {failures} failed, {skipped} skipped (no data)",
"import_csv_data": "Import feedback",
"import_feedback": "Import feedback",
"import_historical_responses": "Import historical responses",
"import_historical_responses_description": "Creates one feedback record for each answer to each question.",
"import_rows": "Import {count} rows",
"import_via_source_name": "Import via \"{sourceName}\"",
"importing_data": "Importing data...",
"importing_historical_data": "Importing historical data...",
"invalid_enum_values": "Invalid values in column mapped to {field}",
"invalid_values_found": "Found: {values} (rows: {rows}) {extra}",
"load_sample_csv": "Load sample CSV",
"manage_directories": "Manage directories",
"manage_feedback_sources": "Manage feedback sources",
"metadata": "Metadata",
"metadata_key": "Metadata key",
"metadata_read_only_entries": "Read-only metadata values (non-string)",
"metadata_value": "Metadata value",
"missing_feedback_source_title": "Missing a feedback source?",
"n_supported_questions": "{count} supported questions",
"no_feedback_record_directory_available": "No feedback record directory assigned to this workspace. Create or assign one first.",
"no_feedback_records": "No feedback records yet. Records will appear here once your connectors start sending data.",
@@ -3639,15 +3685,16 @@
"question_selected": "<strong>{count}</strong> question selected. Each response to these questions will create a new Feedback Record.",
"question_type_not_supported": "This question type is not supported",
"questions_selected": "<strong>{count}</strong> questions selected. Each response to these questions will create a new Feedback Record.",
"records_will_go_to": "Records will go to",
"refresh_feedback_records": "Refresh feedback records",
"refreshing_feedback_records": "Refreshing feedback records...",
"request_feedback_source": "Request it and we will build it!",
"required": "Required",
"save_changes": "Save changes",
"select_a_survey_to_see_questions": "Select a survey to see its questions",
"select_a_value": "Select a value...",
"select_all": "Select all",
"select_feedback_record_directory": "Select a directory",
"select_feedback_record_source_type": "Select source type",
"select_questions": "Select questions",
"select_source_type_description": "Select the type of feedback source you want to connect.",
"select_source_type_prompt": "Select the type of feedback source you want to connect:",
@@ -3656,12 +3703,14 @@
"select_survey_questions_description": "Choose which survey questions should create FeedbackRecords.",
"set_value": "set value",
"setup_connection": "Setup connection",
"showing_count_loaded": "Showing {count} records",
"showing_count_loaded": "Showing {count} records from Feedback Directory {directoryName}",
"showing_rows": "Showing 3 of {count} rows",
"source": "source",
"source_connect_csv_description": "Import feedback from CSV files",
"source_connect_formbricks_description": "Connect feedback from your Formbricks surveys",
"source_connect_feedback_record_mcp_description": "Connect feedback records via the Formbricks MCP.",
"source_connect_formbricks_description": "Connect feedback from your Formbricks survey",
"source_fields": "Source Fields",
"source_id": "Source ID",
"source_name": "Source Name",
"source_type": "Source Type",
"source_type_cannot_be_changed": "Source type cannot be changed",
@@ -3670,9 +3719,13 @@
"status_completed": "Completed",
"status_draft": "Draft",
"status_error": "Error",
"status_live_sync": "Live Sync",
"status_paused": "Paused",
"status_ready": "Ready",
"submission_id": "Submission ID",
"survey_has_no_questions": "This survey has no questions",
"survey_import_line": "{surveyName}: {responseCount} responses × {questionCount} questions = {total} Feedback Records",
"topics_and_subtopics": "Topics & Subtopics",
"total_feedback_records": "Total: {checked} of {total} Feedback Records selected across {surveyCount} surveys",
"unify_feedback": "Unify Feedback",
"update_mapping_description": "Update the mapping configuration for this source.",
@@ -3680,7 +3733,11 @@
"upload_csv_data_description": "Upload a CSV file to import feedback data.",
"upload_csv_file": "Upload CSV File",
"user_identifier": "User",
"value": "Value"
"value": "Value",
"value_boolean": "Value (Boolean)",
"value_date": "Value (Date)",
"value_number": "Value (Number)",
"value_text": "Value (Text)"
},
"xm-templates": {
"ces": "CES",
@@ -160,65 +160,8 @@ export const EditAPIKeys = ({
return (
<div className="space-y-4">
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-10 content-center rounded-t-lg bg-slate-100 px-6 text-left text-sm font-semibold text-slate-900">
<div className="col-span-4 sm:col-span-2">{t("common.label")}</div>
<div className="col-span-4 hidden sm:col-span-5 sm:block">{t("workspace.api_keys.api_key")}</div>
<div className="col-span-4 sm:col-span-2">{t("common.created_at")}</div>
<div></div>
</div>
<div className="grid-cols-9">
{apiKeysLocal?.length === 0 ? (
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm font-medium text-slate-400">
{t("workspace.api_keys.no_api_keys_yet")}
</div>
) : (
apiKeysLocal?.map((apiKey) => (
<div
role="button"
className="grid h-12 w-full grid-cols-10 content-center items-center rounded-lg px-6 text-left text-sm text-slate-900 hover:bg-slate-50 focus:bg-slate-50 focus:outline-none"
onClick={() => {
setActiveKey(apiKey);
setViewPermissionsOpen(true);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setActiveKey(apiKey);
setViewPermissionsOpen(true);
}
}}
tabIndex={0}
data-testid="api-key-row"
key={apiKey.id}>
<div className="col-span-4 font-semibold sm:col-span-2">{apiKey.label}</div>
<div className="col-span-4 hidden pr-4 sm:col-span-5 sm:block">
<ApiKeyDisplay apiKey={apiKey.actualKey ?? ""} />
</div>
<div className="col-span-4 sm:col-span-2">
{timeSince(apiKey.createdAt.toString(), locale)}
</div>
{!isReadOnly && (
<div className="col-span-1 text-center">
<Button
size="icon"
variant="ghost"
onClick={(e) => {
handleOpenDeleteKeyModal(e, apiKey);
e.stopPropagation();
}}>
<TrashIcon />
</Button>
</div>
)}
</div>
))
)}
</div>
</div>
{!isReadOnly && (
<div>
<div className="absolute right-4 top-4">
<Button
size="sm"
onClick={() => {
@@ -228,6 +171,65 @@ export const EditAPIKeys = ({
</Button>
</div>
)}
<div className="rounded-xl border border-slate-200 bg-white shadow-sm">
<div className="grid h-12 grid-cols-10 content-center border-b border-slate-200 px-6 text-left text-sm font-semibold text-slate-900">
<div className="col-span-4 sm:col-span-2">{t("common.label")}</div>
<div className="col-span-4 hidden sm:col-span-5 sm:block">{t("workspace.api_keys.api_key")}</div>
<div className="col-span-4 sm:col-span-2">{t("common.created_at")}</div>
<div></div>
</div>
<div>
{apiKeysLocal?.length === 0 ? (
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm text-slate-400">
{t("workspace.api_keys.no_api_keys_yet")}
</div>
) : (
<div className="divide-y divide-slate-100">
{apiKeysLocal?.map((apiKey) => (
<div
role="button"
className="grid h-12 w-full grid-cols-10 content-center items-center px-6 text-left text-sm text-slate-900 transition-colors hover:bg-slate-50 focus:bg-slate-50 focus:outline-none"
onClick={() => {
setActiveKey(apiKey);
setViewPermissionsOpen(true);
}}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
setActiveKey(apiKey);
setViewPermissionsOpen(true);
}
}}
tabIndex={0}
data-testid="api-key-row"
key={apiKey.id}>
<div className="col-span-4 font-semibold sm:col-span-2">{apiKey.label}</div>
<div className="col-span-4 hidden pr-4 sm:col-span-5 sm:block">
<ApiKeyDisplay apiKey={apiKey.actualKey ?? ""} />
</div>
<div className="col-span-4 sm:col-span-2">
{timeSince(apiKey.createdAt.toString(), locale)}
</div>
{!isReadOnly && (
<div className="col-span-1 text-center">
<Button
size="icon"
variant="ghost"
onClick={(e) => {
handleOpenDeleteKeyModal(e, apiKey);
e.stopPropagation();
}}>
<TrashIcon />
</Button>
</div>
)}
</div>
))}
</div>
)}
</div>
</div>
<AddApiKeyModal
open={isAddAPIKeyModalOpen}
setOpen={setIsAddAPIKeyModalOpen}
@@ -0,0 +1,119 @@
# Question Bank - Linear Ticket Package (3 Scopes)
## Problem statement
Teams currently recreate the same survey questions across workspaces, which slows authors down, creates inconsistent wording, and makes reporting harder to standardize. We need a shared question bank at the organization level so authenticated creators can reuse high-quality questions quickly without breaking existing surveys.
## Success metrics
- Time to add a reusable question into a survey decreases by at least 50%.
- At least 40% of newly created surveys use one or more question bank entries within 60 days.
- Duplicate-question creation rate declines over time (measured by title/similarity heuristics).
- Permission failures are explicit and non-destructive (0 ambiguous auth errors in tracked flows).
## Locked auth model for all scopes
- Sharing boundary: all workspaces in the same organization.
- Publish eligibility: authenticated users with `readWrite` or `manage` in at least one workspace.
- Edit/delete: creator plus org `Owner/Manager`.
- Unpublish: org `Owner/Manager`.
- API keys: keep existing explicit workspace/org scopes (no owner-role inheritance change).
- Deprovisioned creators: ownership transfers to org admin group.
- Audit baseline: `createdBy`, `updatedBy`, timestamps.
---
## Ticket 1 - Scope 1 Core MVP
### Goal
Launch a usable organization-level question bank that supports the full core lifecycle and detached insertion into surveys.
### In scope
- Browse and insert global questions from survey creation/editing flow.
- Create and publish global questions with locked auth model.
- Edit/delete with creator + org admin override model.
- Unpublish with org admin permissions.
- Insert behavior is copy-detached (future question bank edits do not affect already inserted survey questions).
- Basic metadata in UI: title, category, creator, updated time.
- Empty state, unauthorized state, and no-results state messaging.
### Explicitly out of scope
- Approval workflows.
- Certified/locked question sets.
- Sensitive-category permission differences.
- Version history, diff, restore.
- Live-linked question sync into existing surveys.
### Acceptance criteria
- A user with `readWrite/manage` in at least one workspace can create and publish.
- A user without required workspace permission cannot publish and gets clear guidance.
- Creator can edit/delete own global questions.
- Org `Owner/Manager` can edit/delete/unpublish any global question.
- Inserting a question creates an independent survey copy that does not change on later bank edits.
- Unpublishing hides the question from new insertions but does not alter surveys that already copied it.
- All successful mutations capture attribution metadata (`createdBy`/`updatedBy` and timestamps).
### Scope 1 edge-case handling
- Creator loses workspace write access after publishing: question remains available; creator can no longer mutate unless still authorized by current rules.
- Creator is removed/deactivated: ownership is transferred to org admin group without content loss.
- Admin edits creator-owned content: latest updater attribution is visible.
- Cross-workspace discoverability remains consistent inside the same org.
---
## Ticket 2 - Scope 2 Adoption and Operational Quality
### Goal
Increase discoverability and operational reliability so the bank is easy to use at scale for both UI users and scoped API clients.
### In scope
- Search, category filtering, and sorting.
- API-key read/write support under current explicit workspace/org scope model.
- Ownership transfer flow for deprovisioned creators (admin stewardship).
- Basic moderation operations for org admins focused on unpublish/recoverability.
- Audit visibility in product UX (created by, last updated by, published status timestamps).
### Acceptance criteria
- Users can find bank questions by keyword, category, and sort order with predictable results.
- API key mutations respect explicit scope boundaries; out-of-scope writes are rejected clearly.
- Admins can view and complete ownership transfer for deprovisioned creators.
- Admin can recover previously unpublished items using a clear operational flow.
- Audit fields are visible and understandable in bank listing/detail surfaces.
### Scope 2 edge-case handling
- API key with partial workspace grants attempts org-wide mutation: request is denied with explicit reason.
- Large volume of near-duplicate questions: list remains navigable via filters and sort defaults.
- Simultaneous edits by creator and admin: user-facing result is deterministic and auditable.
---
## Ticket 3 - Scope 3 Governance and Enterprise Depth (Optional/Future)
### Goal
Introduce governance controls for organizations that need stronger quality gates and policy enforcement.
### In scope (optional controls)
- Approval gate before global publication.
- Curator workflow for elevated stewardship.
- Certified/locked question sets (or semi-locked variants).
- Sensitive-category restrictions (if compliance needs require).
- Extended versioning: history, fork, restore.
- Org policy controls for publishing standards and lifecycle rules.
### Acceptance criteria
- Governance controls are configurable per organization and can be rolled out progressively.
- Approval workflow supports clear pending/approved/rejected states.
- Certified or locked items are visibly distinct and enforce expected edit restrictions.
- Version history supports safe recovery without impacting existing detached survey copies.
- Policy changes are auditable and reversible.
### Scope 3 edge-case handling
- Approval backlog delays publication: pending state remains visible and actionable.
- Policy changes after content is already published: no retroactive corruption of survey copies.
- Governance disabled for some orgs: core flows continue without governance regressions.
---
## Prioritization rationale (Qualtrics-informed)
- Start with the minimum reusable-library primitives users expect: browse, search, insert, categorize.
- Keep insertion detached by default to prevent retroactive survey changes.
- Defer certified and strict governance controls until adoption data justifies added complexity.
Reference: [Qualtrics pre-made library questions](https://www.qualtrics.com/support/survey-platform/survey-module/editing-questions/question-types-guide/pre-made-qualtrics-library-questions/)