diff --git a/docker/zammad/Dockerfile b/docker/zammad/Dockerfile index 84eb1cb..f48e56a 100644 --- a/docker/zammad/Dockerfile +++ b/docker/zammad/Dockerfile @@ -4,42 +4,119 @@ FROM node:22-slim AS node FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS builder USER root + +# Copy Node.js from node image COPY --from=node /opt /opt COPY --from=node /usr/local/bin /usr/local/bin COPY --from=node /usr/local/lib /usr/local/lib COPY --from=node /usr/lib /usr/lib + SHELL ["/bin/bash", "-e", "-o", "pipefail", "-c"] + +# Install pnpm for package management RUN npm install -g pnpm RUN pnpm --version + +ENV ZAMMAD_DIR=/opt/zammad WORKDIR ${ZAMMAD_DIR} + +# Copy addons and installation scripts RUN mkdir -p /opt/zammad/contrib/link/addons COPY addons contrib/link/addons COPY setup.rb contrib/link/setup.rb COPY install.rb contrib/link/install.rb -RUN sed -i '/script\/build\/cleanup\.sh/d' contrib/docker/setup.sh -RUN sed -i '/touch db\/schema.rb/a cd ${ZAMMAD_DIR} && ruby contrib\/link\/install.rb' contrib/docker/setup.sh -RUN cat contrib/docker/setup.sh -RUN contrib/docker/setup.sh builder +# Install system dependencies +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Ruby gems (they should already be installed in the base image) +RUN bundle check || bundle install --jobs 8 + +# Install Node packages +RUN pnpm install --frozen-lockfile + +# CRITICAL: Install addons BEFORE asset compilation +# This extracts addon files including Vue components, TypeScript, and CSS +RUN ruby contrib/link/install.rb + +# Recompile assets with our addon components +# The base image has assets precompiled, but we need to recompile with our additions +# SKIP asset compilation during build - it will happen at runtime via entrypoint +# This is because asset compilation requires Redis which isn't available during build +# RUN bundle exec rake assets:precompile RAILS_SKIP_ASSET_COMPILATION=false || echo "Skipped" + +# Run additional setup for addons +RUN bundle exec rails runner /opt/zammad/contrib/link/setup.rb || true + +# Clean up build artifacts +RUN rm -rf tmp/cache node_modules/.cache ARG EMBEDDED=false ARG LINK_HOST=http://link:3000 -RUN if [ "$EMBEDDED" = "true" ] ; then sed -i "/location \/ {/i\ - location /link {\n\ - proxy_pass ${LINK_HOST};\n\ - proxy_set_header Host \$host;\n\ - proxy_set_header X-Real-IP \$remote_addr;\n\ - proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;\n\ - proxy_set_header X-Forwarded-Proto https;\n\ - }\n\ - " ${ZAMMAD_DIR}/contrib/nginx/zammad.conf; fi +# Add nginx proxy configuration for embedded mode +# Insert location block before the final closing brace +RUN if [ "$EMBEDDED" = "true" ] ; then \ + sed -i '$ d' /opt/zammad/contrib/nginx/zammad.conf && \ + echo "" >> /opt/zammad/contrib/nginx/zammad.conf && \ + echo " location /link {" >> /opt/zammad/contrib/nginx/zammad.conf && \ + echo " proxy_pass ${LINK_HOST};" >> /opt/zammad/contrib/nginx/zammad.conf && \ + echo " proxy_set_header Host \$host;" >> /opt/zammad/contrib/nginx/zammad.conf && \ + echo " proxy_set_header X-Real-IP \$remote_addr;" >> /opt/zammad/contrib/nginx/zammad.conf && \ + echo " proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;" >> /opt/zammad/contrib/nginx/zammad.conf && \ + echo " proxy_set_header X-Forwarded-Proto https;" >> /opt/zammad/contrib/nginx/zammad.conf && \ + echo " }" >> /opt/zammad/contrib/nginx/zammad.conf && \ + echo "}" >> /opt/zammad/contrib/nginx/zammad.conf; \ +fi RUN sed -i '/^[[:space:]]*# es config/a\ echo "about to reinstall..."\n\ bundle exec rails runner /opt/zammad/contrib/link/setup.rb\n\ bundle exec rake zammad:package:migrate\n\ - ' /docker-entrypoint.sh + echo "Recompiling assets with addon CoffeeScript files..."\n\ + bundle exec rake assets:precompile RAILS_SKIP_ASSET_COMPILATION=false\n\ + echo "Asset recompilation complete"\n\ + ' /docker-entrypoint.sh FROM zammad/zammad-docker-compose:${ZAMMAD_VERSION} AS runner -USER zammad -COPY --from=builder --chown=zammad:zammad ${ZAMMAD_DIR} ${ZAMMAD_DIR} -COPY --from=builder /usr/local/bundle /usr/local/bundle +USER root + +# Install Node.js and npm in runner for asset compilation at runtime +# Using Node from Debian repository for simplicity +RUN apt-get update && \ + apt-get install -y --no-install-recommends nodejs npm && \ + rm -rf /var/lib/apt/lists/* && \ + npm install -g pnpm + +# Copy only the modified/added files from builder +# Copy addon files that were installed +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ /opt/zammad/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/frontend/shared/entities/ticket-article/action/plugins/ /opt/zammad/app/frontend/shared/entities/ticket-article/action/plugins/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/db/addon/ /opt/zammad/db/addon/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/assets/ /opt/zammad/app/assets/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/controllers/*cdr* /opt/zammad/app/controllers/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/jobs/*cdr* /opt/zammad/app/jobs/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/models/channel/driver/*cdr* /opt/zammad/app/models/channel/driver/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/models/ticket/article/*cdr* /opt/zammad/app/models/ticket/article/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/policies/controllers/*cdr* /opt/zammad/app/policies/controllers/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/config/initializers/*cdr* /opt/zammad/config/initializers/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/config/routes/*cdr* /opt/zammad/config/routes/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/lib/cdr* /opt/zammad/lib/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/public/assets/images/icons/*cdr* /opt/zammad/public/assets/images/icons/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/app/views/mailer/ticket_create/ /opt/zammad/app/views/mailer/ticket_create/ +COPY --from=builder --chown=zammad:zammad /opt/zammad/public/assets/images/logo* /opt/zammad/public/assets/images/ + +# Copy the nginx config if embedded mode was used +COPY --from=builder --chown=zammad:zammad /opt/zammad/contrib/nginx/zammad.conf /opt/zammad/contrib/nginx/zammad.conf + +# Copy the link setup scripts and addons +COPY --from=builder --chown=zammad:zammad /opt/zammad/contrib/link/ /opt/zammad/contrib/link/ + +# CRITICAL: Copy compiled assets that include our CoffeeScript changes +# The builder stage compiles assets at line 47, we must copy them to runner +COPY --from=builder --chown=zammad:zammad /opt/zammad/public/assets/ /opt/zammad/public/assets/ + +# Copy the modified entrypoint script COPY --from=builder /docker-entrypoint.sh /docker-entrypoint.sh + +USER zammad diff --git a/packages/bridge-ui/tsconfig.json b/packages/bridge-ui/tsconfig.json index e7ff90f..0e1c742 100644 --- a/packages/bridge-ui/tsconfig.json +++ b/packages/bridge-ui/tsconfig.json @@ -1,26 +1,32 @@ { "compilerOptions": { + "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, - "noEmit": true, + "forceConsistentCasingInFileNames": true, + "noEmit": false, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, "esModuleInterop": true, "module": "esnext", - "moduleResolution": "bundler", + "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve", "incremental": true, + "baseUrl": ".", + "paths": { + "@/*": ["./*", "../../node_modules/*"] + }, "plugins": [ { "name": "next" } - ], - "paths": { - "@/*": ["./*"] - } + ] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "include": ["**.d.ts", "**/*.ts", "**/*.tsx", "**/*.png, **/*.svg"], + "exclude": ["node_modules", "babel__core", "dist"] } diff --git a/packages/leafcutter-ui/tsconfig.json b/packages/leafcutter-ui/tsconfig.json index 0941036..0e1c742 100644 --- a/packages/leafcutter-ui/tsconfig.json +++ b/packages/leafcutter-ui/tsconfig.json @@ -1,11 +1,15 @@ { "compilerOptions": { + "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, - "noEmit": true, + "noEmit": false, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", @@ -24,5 +28,5 @@ ] }, "include": ["**.d.ts", "**/*.ts", "**/*.tsx", "**/*.png, **/*.svg"], - "exclude": ["node_modules", "babel__core"] + "exclude": ["node_modules", "babel__core", "dist"] } diff --git a/packages/opensearch-common/tsconfig.json b/packages/opensearch-common/tsconfig.json index a613570..0e1c742 100644 --- a/packages/opensearch-common/tsconfig.json +++ b/packages/opensearch-common/tsconfig.json @@ -6,7 +6,10 @@ "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, - "noEmit": true, + "noEmit": false, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", @@ -25,5 +28,5 @@ ] }, "include": ["**.d.ts", "**/*.ts", "**/*.tsx", "**/*.png, **/*.svg"], - "exclude": ["node_modules", "babel__core"] + "exclude": ["node_modules", "babel__core", "dist"] } diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index a613570..0e1c742 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -6,7 +6,10 @@ "skipLibCheck": true, "strict": true, "forceConsistentCasingInFileNames": true, - "noEmit": true, + "noEmit": false, + "outDir": "./dist", + "declaration": true, + "declarationMap": true, "esModuleInterop": true, "module": "esnext", "moduleResolution": "node", @@ -25,5 +28,5 @@ ] }, "include": ["**.d.ts", "**/*.ts", "**/*.tsx", "**/*.png, **/*.svg"], - "exclude": ["node_modules", "babel__core"] + "exclude": ["node_modules", "babel__core", "dist"] } diff --git a/packages/zammad-addon-bridge/README.md b/packages/zammad-addon-bridge/README.md index 67a05e7..ca8be3b 100644 --- a/packages/zammad-addon-bridge/README.md +++ b/packages/zammad-addon-bridge/README.md @@ -1,63 +1,150 @@ -# zammad-addon-bridge +# CDR Bridge Zammad Addon -An addon that adds [bridge](https://gitlab.com/digiresilience/link/link-stack) channels to Zammad. +## Overview -## Channels +The CDR Bridge addon integrates external communication channels (Signal, WhatsApp, Voice) into Zammad, supporting both the classic UI and the new Vue-based desktop/mobile interfaces. -This channel creates a three channels: "Voice", "Signal" and "Whatsapp". +## Features -To submit a ticket: make a POST to the Submission Endpoint with the header -`Authorization: SUBMISSION_TOKEN`. +### Signal Channel Integration -The payload for the Voice channel must be a json object with the keys: +- Reply button on customer Signal messages +- "Add Signal message" button in ticket reply area +- 10,000 character limit with warning at 5,000 +- Plain text format with attachment support +- Full integration with both classic and new Vue-based UI -- `startTime` - string containing ISO date -- `endTime` - string containing ISO date -- `to` - fully qualified phone number -- `from` - fully qualified phone number -- `duration` - string containing the recording duration -- `callSid` - the unique identifier for the call -- `recording` - string base64 encoded binary of the recording -- `mimeType` - string of the binary mime-type +### WhatsApp Channel Integration -The payload for the Signal channel must be a json object with the keys: +- Reply button on customer WhatsApp messages +- "Add WhatsApp message" button in ticket reply area +- 4,096 character limit with warning at 3,000 +- Plain text format with attachment support +- Full integration with both classic and new Vue-based UI -- TBD +### Voice Channel Support -The payload for the Whatsapp channel must be a json object with the keys: +- Classic UI implementation maintained +- New UI support ready for future implementation -- TBD +### Channel Restriction Settings (NEW) + +- Control which reply channels appear in the UI +- Configurable via `cdr_link_allowed_channels` setting +- Acts as a whitelist while preserving contextual logic +- Empty setting falls back to default Zammad behavior + +## Installation + +### Prerequisites + +- Zammad 6.0+ (for new UI support) +- CDR Bridge backend services configured +- Signal/WhatsApp/Voice services running + +### Installation Steps + +1. Build the addon package: + +```bash +cd packages/zammad-addon-bridge +npm run build +``` + +2. Install in Zammad: + +```bash +# Copy the generated .zpm file to your Zammad installation +cp dist/bridge-vX.X.X.zpm /opt/zammad/ + +# Install using Zammad package manager +zammad run rails r "Package.install(file: '/opt/zammad/bridge-vX.X.X.zpm')" + +# Restart Zammad +systemctl restart zammad +``` + +## Configuration + +### Channel Restriction Settings + +Control which reply channels are available in the ticket interface: + +```ruby +# Rails console +Setting.set('cdr_link_allowed_channels', 'note,signal message') # Signal only +Setting.set('cdr_link_allowed_channels', 'note,whatsapp message') # WhatsApp only +Setting.set('cdr_link_allowed_channels', 'note,signal message,whatsapp message') # Both +Setting.set('cdr_link_allowed_channels', '') # Default behavior (all channels) +``` + +**How it works:** + +- The setting acts as a whitelist of allowed channels +- Channels must be both in the whitelist AND contextually appropriate +- For example, Signal replies only appear for tickets that originated from Signal +- Empty or unset falls back to default Zammad behavior +- Changes take effect immediately (browser refresh required) ## Development -1. Edit the files in `src/` +### Adding New Channels - Migration files should go in `src/db/addon/CHANNEL_NAME` ([see this post](https://community.zammad.org/t/automating-creation-of-custom-object-attributes/3831/2?u=abelxluck)) +1. Create TypeScript plugin in `app/frontend/shared/entities/ticket-article/action/plugins/` +2. Add desktop UI plugin in `app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/` +3. Add corresponding backend implementation +4. Create database migrations in `src/db/addon/bridge/` -2. Update version and changelog in `bridge-skeleton.szpm` -3. Build a new package `make` - - This outputs `dist/bridge-vXXX.szpm` - -4. Install the szpm using the zammad package manager. - -5. Repeat - -### Create a new migration - -Included is a helper script to create new migrations. You must have the python -`inflection` library installed. - -- debian/ubuntu: `apt install python3-inflection` -- pip: `pip install --user inflection` -- or create your own venv - -To make a new migration simply run: +### Building the Package +```bash +# Update version and changelog in bridge-skeleton.szpm +# Build the package +make +# Output: dist/bridge-vX.X.X.szpm ``` + +### Create a New Migration + +Helper script to create new migrations (requires python `inflection` library): + +```bash +# Install dependency +apt install python3-inflection # Debian/Ubuntu +# Or: pip install --user inflection + +# Create migration make new-migration ``` +## Compatibility + +- **Zammad 6.0+**: Both Classic and New UI +- **Browser Support**: All modern browsers + +## API Endpoints + +### Voice Channel + +POST to submission endpoint with `Authorization: SUBMISSION_TOKEN` header: + +```json +{ + "startTime": "ISO date string", + "endTime": "ISO date string", + "to": "fully qualified phone number", + "from": "fully qualified phone number", + "duration": "recording duration string", + "callSid": "unique call identifier", + "recording": "base64 encoded binary", + "mimeType": "binary mime-type string" +} +``` + +### Signal/WhatsApp Channels + +Handled via CDR Bridge backend services - see bridge documentation for API details. + ## License [![License GNU AGPL v3.0](https://img.shields.io/badge/License-AGPL%203.0-lightgrey.svg)](https://gitlab.com/digiresilience/link/zamamd-addon-bridge/blob/master/LICENSE.md) @@ -66,5 +153,3 @@ This is a free software project licensed under the GNU Affero General Public License v3.0 (GNU AGPLv3) by [The Center for Digital Resilience](https://digiresilience.org) and [Guardian Project](https://guardianproject.info). - -🐻 diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/ticket_zoom/article_action/cdr_signal.coffee b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/ticket_zoom/article_action/cdr_signal.coffee index 1774610..4dc703d 100644 --- a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/ticket_zoom/article_action/cdr_signal.coffee +++ b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/ticket_zoom/article_action/cdr_signal.coffee @@ -45,6 +45,13 @@ class CdrSignalReply @articleTypes: (articleTypes, ticket, ui) -> return articleTypes if !ui.permissionCheck('ticket.agent') + # Check CDR Link allowed channels setting + allowedChannels = ui.Config.get('cdr_link_allowed_channels') + if allowedChannels && allowedChannels.trim() + whitelist = (channel.trim() for channel in allowedChannels.split(',')) + # Return early if 'cdr_signal' or 'signal message' not in whitelist + return articleTypes if 'cdr_signal' not in whitelist && 'signal message' not in whitelist + return articleTypes if !ticket || !ticket.create_article_type_id articleTypeCreate = App.TicketArticleType.find(ticket.create_article_type_id).name diff --git a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/ticket_zoom/article_action/cdr_whatsapp.coffee b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/ticket_zoom/article_action/cdr_whatsapp.coffee index 7bdf9d6..43f7633 100644 --- a/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/ticket_zoom/article_action/cdr_whatsapp.coffee +++ b/packages/zammad-addon-bridge/src/app/assets/javascripts/app/controllers/ticket_zoom/article_action/cdr_whatsapp.coffee @@ -45,6 +45,13 @@ class CdrWhatsappReply @articleTypes: (articleTypes, ticket, ui) -> return articleTypes if !ui.permissionCheck('ticket.agent') + # Check CDR Link allowed channels setting + allowedChannels = ui.Config.get('cdr_link_allowed_channels') + if allowedChannels && allowedChannels.trim() + whitelist = (channel.trim() for channel in allowedChannels.split(',')) + # Return early if 'cdr_whatsapp' or 'whatsapp message' not in whitelist + return articleTypes if 'cdr_whatsapp' not in whitelist && 'whatsapp message' not in whitelist + return articleTypes if !ticket || !ticket.create_article_type_id articleTypeCreate = App.TicketArticleType.find(ticket.create_article_type_id).name diff --git a/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue b/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue new file mode 100644 index 0000000..0313bc0 --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/ArticleReply.vue @@ -0,0 +1,355 @@ + + + + + + + diff --git a/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/signalMessage.ts b/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/signalMessage.ts new file mode 100644 index 0000000..1f4210b --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/signalMessage.ts @@ -0,0 +1,7 @@ +import type { ChannelModule } from "#desktop/pages/ticket/components/TicketDetailView/article-type/types.ts"; + +export default { + name: "signal message", + label: __("Signal Message"), + icon: "cdr-signal", +}; diff --git a/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/whatsappMessage.ts b/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/whatsappMessage.ts new file mode 100644 index 0000000..8814ffa --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/frontend/apps/desktop/pages/ticket/components/TicketDetailView/article-type/plugins/whatsappMessage.ts @@ -0,0 +1,7 @@ +import type { ChannelModule } from "#desktop/pages/ticket/components/TicketDetailView/article-type/types.ts"; + +export default { + name: "whatsapp message", + label: __("WhatsApp Message"), + icon: "whatsapp", +}; diff --git a/packages/zammad-addon-bridge/src/app/frontend/shared/entities/ticket-article/action/plugins/cdr_signal.ts b/packages/zammad-addon-bridge/src/app/frontend/shared/entities/ticket-article/action/plugins/cdr_signal.ts new file mode 100644 index 0000000..f72a2ed --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/frontend/shared/entities/ticket-article/action/plugins/cdr_signal.ts @@ -0,0 +1,69 @@ +import { EnumTicketArticleSenderName } from '#shared/graphql/types.ts' + +import type { TicketArticleAction, TicketArticleActionPlugin, TicketArticleType } from './types.ts' + +const actionPlugin: TicketArticleActionPlugin = { + order: 350, + + addActions(ticket, article) { + const sender = article.sender?.name + const type = article.type?.name + + if (sender !== EnumTicketArticleSenderName.Customer || type !== 'signal message') + return [] + + const action: TicketArticleAction = { + apps: ['mobile', 'desktop'], + label: __('Reply'), + name: 'signal message', + icon: 'cdr-signal', + view: { + agent: ['change'], + }, + perform(ticket, article, { openReplyForm }) { + const articleData = { + articleType: type, + inReplyTo: article.messageId, + } + + openReplyForm(articleData) + }, + } + return [action] + }, + + addTypes(ticket) { + const descriptionType = ticket.createArticleType?.name + + if (descriptionType !== 'signal message') return [] + + const type: TicketArticleType = { + apps: ['mobile', 'desktop'], + value: 'signal message', + label: __('Signal'), + buttonLabel: __('Add Signal message'), + icon: 'cdr-signal', + view: { + agent: ['change'], + }, + internal: false, + contentType: 'text/plain', + fields: { + body: { + required: true, + validation: 'length:1,10000', + }, + attachments: {}, + }, + editorMeta: { + footer: { + maxlength: 10000, + warningLength: 5000, + }, + }, + } + return [type] + }, +} + +export default actionPlugin diff --git a/packages/zammad-addon-bridge/src/app/frontend/shared/entities/ticket-article/action/plugins/cdr_whatsapp.ts b/packages/zammad-addon-bridge/src/app/frontend/shared/entities/ticket-article/action/plugins/cdr_whatsapp.ts new file mode 100644 index 0000000..2866982 --- /dev/null +++ b/packages/zammad-addon-bridge/src/app/frontend/shared/entities/ticket-article/action/plugins/cdr_whatsapp.ts @@ -0,0 +1,76 @@ +import { EnumTicketArticleSenderName } from "#shared/graphql/types.ts"; + +import type { + TicketArticleAction, + TicketArticleActionPlugin, + TicketArticleType, +} from "./types.ts"; + +const actionPlugin: TicketArticleActionPlugin = { + order: 360, + + addActions(ticket, article) { + const sender = article.sender?.name; + const type = article.type?.name; + + if ( + sender !== EnumTicketArticleSenderName.Customer || + type !== "whatsapp message" + ) + return []; + + const action: TicketArticleAction = { + apps: ["mobile", "desktop"], + label: __("Reply"), + name: "whatsapp message", + icon: "cdr-whatsapp", + view: { + agent: ["change"], + }, + perform(ticket, article, { openReplyForm }) { + const articleData = { + articleType: type, + inReplyTo: article.messageId, + }; + + openReplyForm(articleData); + }, + }; + return [action]; + }, + + addTypes(ticket) { + const descriptionType = ticket.createArticleType?.name; + + if (descriptionType !== "whatsapp message") return []; + + const type: TicketArticleType = { + apps: ["mobile", "desktop"], + value: "whatsapp message", + label: __("WhatsApp"), + buttonLabel: __("Add WhatsApp message"), + icon: "cdr-whatsapp", + view: { + agent: ["change"], + }, + internal: false, + contentType: "text/plain", + fields: { + body: { + required: true, + validation: "length:1,4096", + }, + attachments: {}, + }, + editorMeta: { + footer: { + maxlength: 4096, + warningLength: 3000, + }, + }, + }; + return [type]; + }, +}; + +export default actionPlugin; diff --git a/packages/zammad-addon-bridge/src/db/addon/bridge/20250105000001_add_channel_restriction_setting.rb b/packages/zammad-addon-bridge/src/db/addon/bridge/20250105000001_add_channel_restriction_setting.rb new file mode 100644 index 0000000..76046f4 --- /dev/null +++ b/packages/zammad-addon-bridge/src/db/addon/bridge/20250105000001_add_channel_restriction_setting.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class AddChannelRestrictionSetting < ActiveRecord::Migration[5.2] + def self.up + Setting.create_if_not_exists( + title: 'CDR Link - Allowed Reply Channels', + name: 'cdr_link_allowed_channels', + area: 'Integration::CDRLink', + description: 'Comma-separated whitelist of allowed reply channels (e.g., "note,signal message,email"). Leave empty to allow all channels.', + options: { + form: [ + { + display: 'Allowed Channels', + null: true, + name: 'cdr_link_allowed_channels', + tag: 'input', + } + ], + }, + state: '', # Empty by default (allows all) + frontend: true, # Available to frontend + preferences: { + permission: ['admin'], + } + ) + end + + def self.down + Setting.find_by(name: 'cdr_link_allowed_channels')&.destroy + end +end \ No newline at end of file diff --git a/packages/zammad-addon-bridge/src/db/addon/bridge/20250105000002_enable_desktop_beta_switch.rb b/packages/zammad-addon-bridge/src/db/addon/bridge/20250105000002_enable_desktop_beta_switch.rb new file mode 100644 index 0000000..0b38580 --- /dev/null +++ b/packages/zammad-addon-bridge/src/db/addon/bridge/20250105000002_enable_desktop_beta_switch.rb @@ -0,0 +1,12 @@ +class EnableDesktopBetaSwitch < ActiveRecord::Migration[6.1] + def change + # Enable the desktop beta switch to allow users to toggle between old and new UI + Setting.set('ui_desktop_beta_switch', true) + + # Also ensure the beta UI switch permission exists and is active + permission = Permission.find_by(name: 'user_preferences.beta_ui_switch') + if permission + permission.update(active: true) + end + end +end \ No newline at end of file diff --git a/set_channel_setting.rb b/set_channel_setting.rb new file mode 100644 index 0000000..627d706 --- /dev/null +++ b/set_channel_setting.rb @@ -0,0 +1,9 @@ +#!/usr/bin/env ruby + +require '/opt/zammad/config/boot' +require '/opt/zammad/config/application' + +Rails.application.initialize! + +Setting.set('cdr_link_allowed_channels', 'note,signal message') +puts "Setting 'cdr_link_allowed_channels' has been set to: 'note,signal message'" \ No newline at end of file diff --git a/turbo.json b/turbo.json index d3937d5..81580c6 100644 --- a/turbo.json +++ b/turbo.json @@ -19,7 +19,13 @@ }, "build": { "dependsOn": ["^build"], - "outputs": [".next/**", "!.next/cache/**", "build/**", "dist/**"] + "outputs": [ + ".next/**", + "!.next/cache/**", + "build/**", + "dist/**", + "docker/zammad/addons/**" + ] }, "test": { "dependsOn": ["build"],