fondesk のメンション機能を Slack Bolt Framework を使っていい感じにした話

はじめに

この記事は前回取材してもらった記事 社内部活動でfondeskのカスタマイズ版とSlackチャンネル「TLけいさつ」を作った僕の野望 から技術的な要素をもっと細かく書いたものです

使用技術

なんで作ったか

既に同じような機能をもつ bot はあったけどどれもメンバー表を別で持たないといけなかった なので都度更新しないといけないかったりする億劫さないしは運用作業が必要になっちゃう。。

e.g.

閃き

Slack のプロフィールと同期してくれたらいいのでは? と思い、fondesk がいるチャンネルのメンバーのプロフィールを定期的に同期するハンドラーを書きました(Cron)

実装編

HTTP リクエストを実装

Cron をどのようにしようかと迷ってたんですが、今回 PaaS を GAE にしてることもあって HTTPリクエストを受け取れるエンドポイントを作ることにしました。 Slack Bolt Framework は Express のラッパーなので Express Receiver が使えます 以下のようにしてあっという間に呼び出せます

import { ExpressReceiver } from "@slack/bolt";

const receiver = new ExpressReceiver({
    signingSecret: process.env.SLACK_SIGNING_SECRET,
});

receiver.app.get(`/register`, (req, res) => {
    res.sendStatus(200);
    // チャンネルメンバーを取得
    const channel = new Channel(app);
    // 更新
    channel.putChannelMemberFirestore();
});

Cron を実装

GAE の場合は cron.yaml を作成します そして以下のコマンドを打つと cron を設定できます

$ gcloud app deploy app.yml cron.yml

以下は実際に fondeSlack で動かしているものです

cron:
  - description: upsert slack profile into firestore
    url: /register
    schedule: every 1 hours from 9:00 to 18:00
    timezone: Asia/Tokyo

image.png

ちょっと困ったこと

Cron の schedule ですが微妙にやりたいことができなかったです 例えば弊社では基本的に受電する機会は平日に限られます。 なので平日の営業時間 9:00-18:00 までが受電する時間帯です プロフィールの更新作業もこの時間と同じにしたかったのですが、GAE の Cron ドキュメントを見ると、できないようです

↓ こうしたかった... (平日だけ9:00-18:00に1時間に一回更新)

cron:
  - description: upsert slack profile into firestore
    url: /register
    schedule: every mon tue wed thu fri 1 hours from 9:00 to 18:00
    timezone: Asia/Tokyo

参考

プロフィールを DB に保存する

今回、DB はさくっと作りたかったので Firestore を使ってます 構成としては、Slack のプロフィールを DB に保存しておいて、fondesk からメッセージがきたら DB と照らしあわせて、対象のユーザの SlackID でメンションする形です

Slack 命名規則だったり除外したいプロフィール項目だったり

プロフィール項目は特定の項目は除外するようにしてます 弊社でいうと部署ですね

image.png

弊社では Slack の本名には命名規則があり、Asato Nago / 名護 朝人 のようにしています

image.png

このように会社ごとで命名規則などがあると思うのでその点は調整可能なようにしています (これは config ファイルなどを別で管理する予定

    // 検索対象に含めたくないslackのプロフィール項目名
    private IGNORE_CUSTOM_FIELDS_LABEL = ["部署"];

    private splitWords = (data: string): string[] => {
        if (!data) return [];

        // チームによってSlackプロフィールの設定ルールがあると思うのでここは区切りたい文字で
        const specialChar = /\/||//;
        const result = data
            .split(/\s+/)
            .filter(item => item.replace(specialChar, ""));
        console.log({ data });
        return result;
    };

    private getCustomFields = (data: fields): string[] => {
        if (!data) return [];
        return Object.keys(data).flatMap(element => {
            const field = data[element];
            if (this.IGNORE_CUSTOM_FIELDS_LABEL.includes(field.label)) {
                return "";
            }
            return this.splitWords(field.value);
        });
    };

実際の Firestore Console

駆け足で作ったのと初めての Firestore なので DB 構成などはもっと良くなると思います 何か tips などあればぜひ教えてほしいです

image.png

Event Subscriptions

さて、最後は fondesk からのメッセージから DB に問合せてメンションする処理です 12/2 から fondesk のペリカンアップデートによって Slack の通知方法が変わりました 具体的には 旧い attachment から block kit を使うようになり、とてもみやすい&わかりやすい内容となってました

Before

image.png

After

image.png

なお、コード側では fondesk のメッセージにのみ反応’するようにしています

app.message(/^(.*)/, async ({ context, message: payload }) => {
    console.log({ payload });

    if (
        payload.subtype !== "bot_message" ||
        !payload.blocks ||
        !payload.blocks.length
    ) {
        console.log("🥺 not fondesk message");
        return;
    }

誰に対してのメッセージかは正規表現で抜き出し、DB に問合せます

    const splitedText = payload.text.match(/あて先:(.+)\s/);
    const targetMember = splitedText.length > 1 ? splitedText[1] : "";
    const hitUser = await getFirestore(targetMember);

あとは、複数人ヒットした場合はメンションを追加したり、また誰も見つからなかった場合のみ @here をつけるようにしたり、メンションすべき人が見つかった場合はスレッドメッセージにしたりしています

誰も見つからなかったとき

image.png

複数人のメンション

image.png

さいごに

先人たちに感謝! そしてお気軽に PR やイシューを是非お願いします〜 75asa/fondeSlack

【Laravel】静的コンテンツを外部管理した際にRedisがパンクした

はじめに

タンバリン東京開発チームの鈴木です。よろしくお願いします。

Laravel開発で、jsやcss、画像ファイルをプロジェクト管理ではなく
外部管理(ファイルサーバ等)とした際に、セッションメモリがパンクし、
静的コンテンツが表示されたり、されなかったりしていた。

こちらの原因・解決までを書いていこうと思います。

静的コンテンツ取得のための設計

通常、静的コンテンツをプロジェクト内に配置した場合、
ルーティングの設定は必要なく、HTML等のソース内にファイルパスを入れるだけで参照できるかと思います。

今回は外部管理となることから、

  1. 外部ファイルURLをGuzzle等のライブラリからGETで呼び出す

  2. ファイル情報を取得

  3. フロントに情報を返す

上記手順を踏む必要があり、外部ファイル参照用のルーティング設定が必要でした。

ルーティング設定

外部ファイル参照用のルーティングとして以下を「./routes/web.php」に設定。

【./routes/web.php】

Route::get('{otherPages}', 'StaticFileController@index')
    ->where(['otherPages' => '.*']);

URL構成として「./hoge」の他に「./hoge/fuga」や「./hoge/fuga/piyo」等の複数ディレクトリ構成にも対応するため、 where句でワイルドカードを指定しています。

上記ルーティングを追加することで、指定されていないURLにアクセスした場合に「StaticFileController」が呼ばれるようになります。

StaticFileController内で、Guzzle等のライブラリを利用し、外部URLに対してGET呼び出し→情報取得を行いますが、今回の原因とは無関係のため省略させていただきます。

本事象発覚

動作環境に上げてから事象が発覚。 外部から取得している画像で404エラーが発生しました。必ず、エラーとなるわけではなく取得できる場合と、出来ない場合が発生しました。

404エラーとなる画像パスに、直アクセスした場合は正常に表示されるため 事象発覚時点では、Laravel内のソースに問題はないと過信しキャッシュサーバーとして利用しているRedisの設定等を疑っていました。

原因調査

Redisを確認したところ以下エラーが出ていることが判明。 内容としては、クライアント数が最大に達したとのエラー。

Exception 'Predis\Connection\ConnectionException' with message 'AUTH failed: ERR max number of clients reached []' in /app/vendor/predis/predis/src/Connection/AbstractConnection.php:155

テスト環境ということもあり、Redisがパンクするほどのアクセスは発生しておらずこちらを疑いました。
Laravelはルーティングを経由したアクセスをした際、CSRFトークンが自動で発行するよう標準実装されています。 「./app/Providers/RouteServiceProvider.php」内で「./routes/web.php」に記述したルーティングに対して適応するミドルウェアが指定されています。

【./app/Providers/RouteServiceProvider.php】

Route::middleware('web')
    ->namespace($this->namespace)
    ->group(base_path('routes/web.php'));

「./app/Http/Kernel.php」内、webグループのミドルウェアが各ルーティングに自動で適応されています。

【./app/Http/Kernel.php】
protected $middlewareGroups = [
    'web' => [
        \App\Http\Middleware\EncryptCookies::class,
        \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
        \Illuminate\Session\Middleware\StartSession::class,
        // \Illuminate\Session\Middleware\AuthenticateSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
        \App\Http\Middleware\VerifyCsrfToken::class,
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],

    'api' => [
        'throttle:60,1',
        \Illuminate\Routing\Middleware\SubstituteBindings::class,
    ],
];

発行されたCSRFトークンはRedisに保存していました。
通常、コントローラーリクエストの1CSRFトークンのみ発行・保存されます。

静的コンテンツを外部管理としたことで、例えば1ページに100画像あった場合、
「コントローラーリクエスト + 100画像リクエスト = 101 CSRFトークン」がアクセスされるたびに発行されRedisに保存されていました。 実際にRedisの中身を確認すると、URLに入るのは、アクセスしたページのURLのはずですが、画像パスに対してCSRFトークンが保存されていました。

【Redis内容】

a:4:{
  s:6:"_token";
  s:40:"xxxxxxxxxx";
  b:1;
  s:9:"_previous";
  a:1:{
    s:3:"url";
    s:55:"xxxxx.png";
  }
  s:6:"_flash";
  a:2:{
    s:3:"old";
    a:0:{}
    s:3:"new";
    a:0:{}
  }
}

アクセスする度に、無駄なCSRFトークンがRedisに保存されたことで
RedisがパンクしCSRFトークンの保存ができないことでエラーが発生。

解決

静的コンテンツ参照用のルーティングでCSRFトークンが発行されることが原因ということで、 トークンが発行されないようルーティングの修正を行いました。

簡単な修正として、前述した「./app/Http/Kernel.php」内webグループから、対象のミドルウェアをコメントアウトすることで対応は可能でした。
しかし、上記対応を取った場合、通常のルーティングでもCSRFトークンが発行されなくなるためセキュリティリスクがあり対応として見送り。

調べたところLaravelには、「withoutMiddleware」という便利な機能があるではありませんか!

withoutMiddlewareとは?
指定したミドルウェアを適用しないようする標準機能

readouble.com

こちらの機能を静的コンテンツ参照用のルーティングに対して追加。

【./routes/web.php】

Route::get('{otherPages}', 'StaticFileController@index')
    ->where(['otherPages' => '.*'])
    ->withoutMiddleware([
        \Illuminate\Session\Middleware\StartSession::class,
        \Illuminate\View\Middleware\ShareErrorsFromSession::class,
    ]);

セッション発行自体が不要なため、セッションの開始から除外とすることで無駄なCSRFトークンの発行がなくなり本件解決となりました…!

まとめ

自分自身のコードの過信は良くないと改めて。 また、標準機能(ミドルウェア)の動きを理解することの大切さを学ぶことが出来ました。

静的コンテンツを外部で管理するレアケースではありますが、参考になればと思います。 以上です!

【LWC】CSVアップロードコンポーネントを作成

さいしょに

こんにちは、タンバリン東京開発チームの岩澤です。

Salesforceが公開しているUI開発フレームワークの中でも比較的新しいLightning Web Componentにふれる機会が多いので、本日はLightning Web ComponentでCSVアップロードコンポーネントを作成してSalesforce標準オブジェクトにデータを追加する方法を書いていこうかと思います。

※ Lightning Web Componentは以降LWCと略します。
※ Salesforceは以降SFと略します。

前提知識としては以下のTrailheadで環境構築、基本的なLWCの扱いは完了しているものとします。

クイックスタート: Lightning Web コンポーネント | Salesforce Trailhead

工程

以下の工程で実装していきます。

  1. CSVファイルの準備
  2. アップロード画面のコンポーネントをつくる
  3. JSでアップロードしたデータを取得
  4. インポート処理を実行

順番にみていきましょう。

CSVファイルの準備

今回はSFの標準オブジェクトである取引先(Account)への登録を行いますので、対象にする数件の項目に対するCSVファイルを用意しました。

以下、CSVファイルを使用します。

account.csv

取引先名,取引先 部門,都道府県 (請求先),電話,種別
user1,部門1,千葉,080-0000-0000,Customor
user2,部門1,東京,080-1111-1111,Customor
user3,部門1,神奈川,080-2222-2222,Customor
user4,部門1,埼玉,080-3333-3333,Customor

DE環境のデフォルト設定で取引先オブジェクトのページに表示されていた項目になります。 都道府県(請求先)は国などその他の項目がマージされて構成された住所(請求先)オブジェクトの一部でオブジェクトマナージャーなどでは見かけからず、API名が見つけづらいので公式ページを参考にします。

https://help.salesforce.com/articleView?id=000323000&language=ja&type=1&mode=1

また、SFに限った話ではないですがCSVファイルを扱う時は最終行に改行がないことを確認してください。 改行をフィールドと認識してしてフィールドが空となりエラーになります。今回のコードでは改行処理はしていないのであしからず。

アップロード画面のコンポーネントをつくる

クイックスタート: Lightning Web コンポーネント | Salesforce Trailhead

上記を参考にcommand ⌘+shift ⇧+pSFDX: Create Lightning Web Componentを選択→csvtest(適当)を入力してEnterEnterでデフォルトのlwcフォルダ配下にコンポーネントの雛形が作成されます。

作成されたコンポーネントのフォルダ配下にcsvtest.htmlファイルがありますのでに以下の修正を加えます。

force-app/main/default/lwc/csvtest/csvtest.html

<template>
  <lightning-card
    title="CSVアップロード"
    icon-name="custom:custom15"
  >
    <div style="padding: 0 20px;">
      <div style="
        padding: 10px 0 0 0;
      ">
        <lightning-input
          type="file"
          name="csv"
          label="CSV"
          onchange={handleCsvUpload}
          accept="text/csv"
        ></lightning-input>
      </div>
      <p style="padding: 10px 0 0 0;">{fileName}</p>
      <div style="
        padding: 20px 0 10px 0;
        margin: 0 0 0 -5px;
      ">
        <div
          if:true={isLoaded}
          class="slds-is-relative"
          style="
            height: 60px;
            margin: 0 0 -10px 0;
          "
        >
          <lightning-spinner alternative-text="Loading..." variant="brand"></lightning-spinner>
        </div>
        <div if:false={isLoaded}>
          <div if:true={isSend}>
            <lightning-button
                variant="brand"
                label="送信"
                title="Primary action"
                onclick={handleUpload}
                class="slds-m-left_x-small">
            </lightning-button>
          </div>
          <div if:false={isSend}>
            <lightning-button
              variant="brand"
              label="送信"
              disabled
              title="Primary action"
              onclick={handleUpload}
              class="slds-m-left_x-small">
            </lightning-button>
          </div>
        </div>
      </div>
    </div>
  </lightning-card>
</template>

<lightning-input type="file>でファイル選択ができるようになります。

ファイルを選択するとif:true={isSend}になり送信ボタンが押せるようになり、送信ボタンを押すとif:true={isLoaded}になりロード画面が表示されアップロードが始まります。

これらの変数はJS側で定義しています。

また、CSSはインラインで直接書いていますが、同じディレクトリ内でcsvtest.cssファイルを作成してCSSを書けばファイルを分けることができます。

続いて、コンポーネントの表示の場所を定義していきます。

今回はホームタブの画面にコンポーネントを表示しますのでcsvtest.js-meta.xmlに以下の修正を加えます。

force-app/main/default/lwc/csvtest/csvtest.js-meta.xml

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>48.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

<targets>要素でSalesforceのどこに表示するか決めている程度の理解でいったん大丈夫な気がします。

ここまで出来ましたら、いったんデプロイしてみましょう。

VSCodeの左側のディレクトリツリーのforce-app/main/defaultの部分を右クリックしてSFDX: Deploy Source to Orgをクリックします。

f:id:AkihitoIwasawaTambourine:20201022182813p:plain

無事にデプロイが完了しましたら、以下の順序で画面に表示します。

command ⌘+shift ⇧+pSFDX: Open Default Orgをクリックしてブラウザ表示

設定から編集ページを開く

f:id:AkihitoIwasawaTambourine:20201022183037p:plain

・カスタムコンポーネントをキャンバスにドラッグして保存

f:id:AkihitoIwasawaTambourine:20201022183104p:plain

以上で設定が反映されホームにアップロード画面が表示されます。

補足:カスタムコンポーネントが表示されなかった場合

編集ページを開いた時にメニューのカスタムにコンポーネントが表示されない場合があります。

原因は私のドメインが設定されていないことが原因です。

f:id:AkihitoIwasawaTambourine:20201022183410p:plain

画面の左下[私のドメイン]をリリースをクリックしてください。

そうしますと以下のようにドメイン名を設定する画面に遷移しますので、お好みのドメインを入力して使用可能か調べるボタンをクリックしまして、問題ないようでしたらドメインの登録をクリックします。

f:id:AkihitoIwasawaTambourine:20201022183448p:plain

ちょっとした注意なのですが、この設定したドメインは一度設定すると自分では変更出来ず、変更する場合は問合せする必要があります。

自分の開発環境ならなんでもいいですが、僕の場合は案件のサンドボックス環境のドメインに自分の名前を入れてしまうというミスをしました。。

こちらも開発環境ではあるから問題ないとは言っていただいたのですが、本番環境だとしたら結構しんどいことになってたかと思いますので、実務の場合は一度ドメインは相談して決めるのがいいかと思います。

登録処理をしますと以下のような画面が表示されます。

f:id:AkihitoIwasawaTambourine:20201023112233p:plain

反映まで少し時間がかかるようです。数分で反映されますのでちょこっと休憩しましょう。

メールでお知らせするとのことですが、メールを確認しなくても少ししますと以下のような画面が表示されますので、ログインボタンをクリックします。

設定したドメインに変更されたトップ画面に遷移されますので、ここまで完了したらカスタムコンポーネントが認識されています。

f:id:AkihitoIwasawaTambourine:20201023112303p:plain

JSでアップロードしたデータを取得

画面のコーディングは完了しましたので、実際の処理を実装していきます。

SalesForce(以降、SF)側にアップロードする処理自体はApexで行いますので、JS側ではApex側に渡すための選択したアップロードのファイルを取得したり、フロント側のバリデーションの処理などを実装してます。

まずは全体のコードを確認しましょう。以下のフォルダ配下にありますcsvtest.jsに以下の変更を加えてください。

force-app/main/default/lwc/csvtest/csvtest.js

// Lightning Web Componentから @wire, @api をインポート
import { LightningElement, api } from 'lwc';
// ShowToastEvent(ポップアップメッセージ)をインポート
import {ShowToastEvent} from 'lightning/platformShowToastEvent';
// Apex Classの定義
import insertCsvData from "@salesforce/apex/csvUploder.insertCsvData";


export default class CsvUploader extends LightningElement {

  // アップロードファイル
  @api file = null;
  // ファイル名(画面表示用)
  @api fileName;
  // 送信フラグ
  @api isSend = false;
  @api isLoaded = false;
  // ファイルの中身
  data;

  // ファイルを選択すると発火
  handleCsvUpload(event) {
    // 選択したアップロードファイルを取得
    this.file = event.detail.files;

    // ファイル名を取得
    this.fileName = this.file[0].name;
    // ファイルが選択されたらボタンをアクティブ化
    this.isSend = this.file;

    // FileReaderオブジェクトの生成
    const fileReader = new FileReader();

    // ファイルの読み込みが完了したら実行
    fileReader.onloadend = () => {
      // 読み込み結果を設定
      this.data = fileReader.result;
    }

    // ファイルを読み込み
    fileReader.readAsText(this.file[0]);
  }

  // csvアップロード処理
  handleUpload() {

    // アップロードファイルを選択していないと送信させない
    if (!this.file) {
      return
    }
    // ローディング表示をtrue
    this.isLoaded = true;

    Promise.resolve().then(() => {
      return new Promise((resolve, reject) => {

        // ファイルを改行で分割しフィールドごとに配列で取得
        const rows = this.data.split(/\r\n|\n/);
        console.log(rows);

        // データをjson形式で取得
        const outputJson = [];
        for(let i=1 ; i<rows.length ; i++) {
          const cols = rows[i].split(',');
          outputJson.push({
            Name: cols[0],
            Site: cols[1],
            BillingState: cols[2],
            Phone: cols[3],
            Type: cols[4],
          });
        }

        // Apexでインポート処理
        insertCsvData({
          jsonData: JSON.stringify(outputJson)
        })
        .then(result => {

          this.data = result;
          resolve(this.data);
          // 成功ポップアップメッセージ表示
          this.dispatchEvent(
            new ShowToastEvent({
              title: 'Success!!',
              message: 'Success!!',
              variant: 'success',
            }),
          );
        })
        // 失敗ポップアップメッセージ表示
        .catch(error => {
          this.dispatchEvent(
            new ShowToastEvent({
              title: 'Error!!',
              message: error,
              variant: 'error',
            }),
          );
        })
      });
    })
    .catch(error => {
      console.log(error)
    })
    // 最終処理
    .finally(() => {
      // input fileを初期化
      this.template.querySelectorAll('lightning-input').forEach(each => {
        each.value = "";
      });
      // アップロードファイルを初期化
      this.file = null;

      // 送信ボタンを非アクティブ化
      this.isLoaded = false;
    });
  }
}

大体はコメントに処理内容を書いてますので、簡単に説明します。

・ファイル選択をしますとhandleCsvUploadメソッドが発火します。csvtest.html<lightning-input>にイベントで定義してます。次に以下の処理が実行されます。

  1. アップロードファイルを取得
  2. 取得ファイルでfileReaderオブジェクトを生成
  3. readAsTextでファイルの中身を文字列で取得

・送信ボタンクリックしますとhandleUploadメソッドが発火します。次に以下の処理が実行されます

  1. ファイルのデータ内容を改行(レコードごと)で分割し、ループを回す
  2. カンマ(フィールドごと)で分割、SFのフィールド名: フィールドの値でJson形式の値を取得
  3. JsonデータをApexへ渡す

インポート処理を実行

インポート処理の前にひとつ確認しておくことがあります。

JSのコードの冒頭にありますimport readCSV from '@salesforce/apex/csvUploder.insertCsvData';です。

これはApexのcsvUploderクラスのinsertCsvDataメソッドをreadCSVで宣言しますという意味です。

ApexとはSalesforceが開発したJavaライクなプログラミング言語になります。

ですので、まだApexクラスもメソッドも作ってないのでファイルを作る必要があります。

command ⌘+shift ⇧+pSFDX: Create Apex Classを選択→csvUploder(適当)を入力してEnterEnterでデフォルトのclassesフォルダ配下にファイルが生成されます。

ファイルが作成されましたらcsvUploder.clsを以下のように編集してください。

force-app/main/default/classes/csvUploder.cls

public with sharing class csvUploder {
    @AuraEnabled
    public static void insertCsvData(String jsonData) {

        // importデータ格納用変数
        List<Account> importData = new List<Account>();
        // 引数のデータをリストで取得
        List<Object> readData = (List<Object>)JSON.deserializeUntyped(jsonData);
        System.debug(readData);
        // リストをループ
        for(Object o : readData) {
            Map<String, Object> m = (Map<String, Object>)o;

            // 項目の値を取得
            Account a = new Account();
            a.Name = (String)m.get('Name');
            a.Site = (String)m.get('Site');
            a.BillingState= (String)m.get('BillingState');
            a.Phone= (String)m.get('Phone');
            a.Type = (String)m.get('Type');

            importData.add(a);
        }
        try {
            insert importData;
        }
        catch(DmlException e) {
            throw e;
        }
    }
}

簡単に説明しますと以下の処理を行っています。

  1. 引数のJsonデータをリストオブジェクトにしてループ
  2. 対象のフィールド名に対してデータをセット
  3. インポート処理

以上でコーディングは完了です。

デプロイして実際にCSVファイルをアップロードしてみるとAccount(取引先)にCSVのデータが挿入されていることが確認出来ます。

以上です。

会議に蔓延るオサレなカタカナ語を斬ってみる

こんにちは、タンバリンのイノウエです。

前置き

当ブログを読んでいる諸氏は、会議中に以下のような台詞を聞いたことがありますでしょうか。

「この件リスケしといて」だの、「この技術のフィジビリティをPoCして、危なそうだったらアズイズで進めよう」だの...

このような「他者への伝達性能」よりも「響きの良さ」に重きを置いたカタカナ語に日頃から辟易している筆者ですが、本記事では、これまでに出会った「オサレカタカナ語」をいくつか紹介しようと思います。

注意事項

本記事の内容は個人的な見解であり、また、使用者自体を否定することを目的とした記事ではありません。

忌むべき「オサレカタカナ語」たち

①オミット:除外する

出典:https://biz.trans-suite.jp/13147

②コンセンサス:合意を取る

出典:https://wa3.i-3-i.info/word13443.html

③マイルストーン:案件の全体図における中間地点などの一区切りとなる場所のこと

出典:https://backlog.com/ja/blog/what-is-milestone-in-project-management/

④フィジビリティ:実現可能性

出典:https://chewy.jp/businessmanner/3911/

⑤PoC:概念実証、技術検証

出典:https://www.softbank.jp/biz/future_stride/entry/technology/20200710/

⑤アズイズ:保証無しで

出典:https://dictionary.goo.ne.jp/word/%E3%82%A2%E3%82%BA%E3%82%A4%E3%82%BA/

⑥トピック:議題、テーマ:主題、サブジェクト:題目 サブジェクト>テーマ>トピックの様です。

出典:https://do-you-imi.net/topic/147/

⑦フィットギャップ:利用者の求めるものと製品の特徴の間の乖離を埋めるために検討すること

出典:http://e-words.jp/w/%E3%83%95%E3%82%A3%E3%83%83%E3%83%88%EF%BC%86%E3%82%AE%E3%83%A3%E3%83%83%E3%83%97%E5%88%86%E6%9E%90.html

⑧ワイヤーフレーム:設計図

出典:https://nandemo-nobiru.com/web-5695

⑨ボトルネック:全体の能力や成果に影響する問題となる要因

出典:https://gakumado.mynavi.jp/freshers/articles/43640

⑩リスケ:予定の再調整

出典:https://www.kaonavi.jp/dictionary/risuke/

最後に

日本語化することが難しい「話し手の意図」を聞き手に伝えるために用いる手段として、英単語を逃げ道とするのではなく、多少時間が掛かってでも日本語を噛み砕いて使用する方が、双方にとってより良い結果となります。

無闇によくわからないものを「響きの良さ」だけで使わずに、聞き手への伝わりやすさ(思いやり)を考える方が、組織を形成する一員として大事なことだと思います。

といったところで、トゥデイのアーティクルはこの辺りでエンドさせてプリーズ。シーユーアゲインです。

Vue.js最新版「Vue 3」を試してみた

f:id:tamb_kgn:20201007180846p:plain

はじめに

初登場のタンバリン大阪開発チームの加减です。よろしくお願いします。

今回は9月19日に正式リリースされたばかりのVue.jsの最新版「Vue 3」を触ってみたいと思います。

実行環境 バージョン
OS MacOS 10.15.6
Node.js 11.15.0
Vue CLI 4.5.6

インストール

とりあえず公式ドキュメントに沿ってやってみましょう。

v3.vuejs.org

いくつか方法がありますが、今回はVue CLIを使っていきます。

npm install -g @vue/cli
vue --version
@vue/cli 4.5.6

Vue CLIの準備ができたのでプロジェクトを作っていきます

vue create vue3-sample
Vue CLI v4.5.6
? Please pick a preset: Default (Vue 3 Preview) ([Vue 3] babel, eslint)

完了するまでちょっと時間がかかるのでコーヒーでも飲みながら待ちましょう

f:id:tamb_kgn:20201001004129p:plain

おっ!どうやら成功したようですね。指示通りに開発用のローカルサーバーを立ち上げてみましょう

cd vue3-sample
npm run serve

f:id:tamb_kgn:20201001004414p:plain

Oops... cannot find module 'vue-loader-v16/package.json' ok...

npm i --save-dev vue-loader-v16
+ vue-loader-v16@16.0.0-beta.5.4
added 9 packages from 43 contributors, updated 8 packages and audited 1326 packages in 7.039s
found 0 vulnerabilities

これでどやあ!

f:id:tamb_kgn:20201001005107p:plain

はい、お待たせしました。インストール完了です。

Composition API

Vue3に追加された機能の中でも、今回は「Composition API」にフォーカスしたいと思います。ちなみにVue.js 2系(以下Vue2)の「Options API」も引き続きサポートされています。 Composition APIの概要について、公式ドキュメントによると↓

Introduction | Vue.js

どうやらOptions APIだとdata, methods, computed, watchといったオプション単位でロジックを書く必要があり、コンポーネントが大きくなるにつれ複数機能の実装がコードの各所に散らばります。

結果、非常に読みづらいのに加え、ロジックの再利用性も低くなってしまいます。Composition APIではこの問題を解決してくれる、ということのようです。

ま、とりあえず触ってみましょう。

今回はサンプルとして、Vue2で書いた簡単なToDoリストアプリをVue3にリプレイスしてみたいと思います。全体のコードは以下から。

github.com

こんな感じの超シンプルなアプリです。

f:id:tamb_kgn:20201006190123g:plain

setup

大前提として、Composition APIではコンポーネントの処理をまとめてsetupメソッド内に定義します。 Vue2ではリアクティブデータはdataメソッド内で扱っていましたが、Vue3では新たに追加されたメソッドrefreactiveを利用して定義します。 それぞれ、reactiveはobject型、refはそれ以外のプリミティブ値に用います。使う際は忘れずimportしてください。

さらに、定義したリアクティブデータをtemplate内で使用するには最後にreturnしてあげる必要があります。

<script>
import { reactive, ref } from 'vue';
export default {
    name: 'Sample',
    props: {
        inputText: String
    },
    //data() {
    //  return {
    //      input: 'Add task here',
    //      items: [],
    //      id: 0
    //  }
    //},
    setup() {
        let input = ref('');
        let items = reactive([]);
        let id = ref(0);
        return {
            input,
            items,
        }
    }
}
</script>

methods

続いてmethodsの定義ですが、非常にシンプルです。ES6そのままな感じ。 注意点として、リアクティブデータの値にアクセスするには変数名の後ろに.valueを付ける必要があります。 ちなみにtemplate側では.valueなしで大丈夫です。

また、Vue2ではthisでVueインスタンスを参照した上でプロパティにアクセスしていましたが、setup内ではthisは不要です。そもそもアクセスできません。

理由は、setupが実行されるのがVueインスタンスが生成される前のbeforeCreateおよびcreatedのタイミングだからです。

<script>
import { reactive, ref } from 'vue';
export default {
    name: 'Sample',
    props: {
        inputText: String
    },
    // methods: {
    //     addListItem() {
    //         this.id++;
    //         const item = {
    //             id: this.id,
    //             text: this.input,
    //             isDone: false,
    //         }
    //         this.items.push(item);
    //         localStorage.setItem('taskList', JSON.stringify(this.items));
    //         localStorage.setItem('lastID', JSON.stringify(item.id));
    //         this.input = '';
    //     },
    //},
    setup() {
        // 省略
        const addListItem = () => {
            if (input.value !== '') {
                id.value++;
                const item = {
                    id: id.value,
                    text: input.value,
                    isDone: false
                }
                items.push(item)
                localStorage.setItem('taskList', JSON.stringify(items));
                localStorage.setItem('lastID', JSON.stringify(item.id));
                input.value = '';
            }
        }
        return {
            input,
            items,
            addListItem,
        }
    }
}
</script>

computed

次はcomputedです。算出プロパティと呼んだりもしますね。

こちらもimportした上でcomputedメソッドを定義していきます。引数にコールバック関数で処理を追加します。

<script>
import { reactive, ref, computed } from 'vue';
export default {
    name: 'Sample',
    props: {
        inputText: String
    },
    // computed: {
    //     doingNum: function() {
    //         return this.items.filter(item => item.isDone === false).length;
    //     }
    // },
    setup() {
        // 省略
        const doingNum = computed(() => {
            return items.filter(item => item.isDone === false).length;
        });
        return {
            input,
            items,
            addListItem,
            doingNum
        }
    }
}
</script>

mounted

Vue3でもライフサイクルフックは健在です。 ただ呼び出し方が少し変更されています。

hook名の頭にonを付けたメソッドとして定義し、引数にコールバック関数を指定します。

<script>
import { reactive, ref, computed, onMounted } from 'vue';
export default {
    name: 'Sample',
    props: {
        inputText: String
    },
    // mounted: function() {
    //         const tasks = JSON.parse(localStorage.getItem('taskList'));
    //         if (tasks != null && tasks.length > 0) {
    //             this.id = JSON.parse(localStorage.getItem('lastID'));
    //             tasks.forEach(task => {
    //                 this.items.push(task);
    //             });
    //         }
    //     }
    setup() {
        // 省略
        const setListItem = () => {
            const tasks = JSON.parse(localStorage.getItem('taskList'));
            if (tasks != null && tasks.length > 0) {
                id.value = JSON.parse(localStorage.getItem('lastID'));
                tasks.forEach(task => {
                    items.push(task);
                });
            }
        }
        onMounted(setListItem);
        return {
            input,
            items,
            addListItem,
            doingNum
        }
    }
}
</script>

onMounted以外のhookについては公式ドキュメントを参照してみてください。

Lifecycle Hooks | Vue.js

Vue.js devtools

chrome.google.com

Chrome等でVue.jsアプリを開発する際に便利な拡張機能「Vue devtool」ですが、残念なことに2020年10月時点ではVue3に未対応のようです。

ですが、ありました解決策が↓

chrome.google.com

β版ですが問題なく使えましたので、開発のお供にどうぞ。

まとめ

Vue.jsを久々に触りましたが、Vue3のComposition APIでだいぶ書きやすくなりましたね。

今回ご紹介したComposition API以外にも、便利な機能がいくつか追加されているようですし、TypeScriptとの親和性アップや、パフォーマンス向上、バンドルサイズの圧縮等々、全体的にパワーアップしているようです。

ただ、現時点ではIE11をサポートしていないようですので、実際のプロジェクトで使う場合はサポートブラウザの要件確認が必須です。将来的にはサポートを予定しているようです。

ふりかえり手法の紹介:感謝(Appreciations)

こんにちは、タンバリンの髙橋です。

ふりかえりの「感謝」(Appreciations)がとても良かったので紹介します。

「感謝」とは言葉の通り、チームメンバーがお互いに感謝し合う活動です。

  • 言わなくても感謝の気持ちは伝わってるはず!
  • ありがとうはいつも言ってる!

その感謝の気持ち、改めてお伝えしてみませんか?

実際に「感謝」のワークをやってみた時のやり方を紹介します。

実施の事前準備

  • 道具を用意する:付箋、サインペン、模造紙やホワイトボード
    • オンライン実施の場合は、使用ツールがあれば事前動作確認の依頼必須です。
    • trelloやJamboard、miro、DropboxPaper、スプレッドシートなどお好みで。
    • グラフィカルにやりたいか、議事録で二次利用したいか等を判断材料に。
  • 参加者の招待、時間・場所の確保(5分〜30分くらい)
    • 6人のチームメンバーで実施した時は、15分くらいでした
  • 工程の終わりや、プロジェクトの終わりなど、区切りのタイミングのふりかえりと合わせてやってみる

実施

0. 案内

助けてくれたり、貢献してくれたり、問題を解決してくれたりしたメンバーに感謝します。 感謝はあくまで任意、強制ではありません。

1. デモンストレーション

「今回のふりかえり期間(リリース、プロジェクト)で、みんなの貢献に関して、お互いに感謝しあいましょう」と言い、デモンストレーションを行います。

相手の名前を呼んでから、 「◯◯さん、あなたの XXXX に私は感謝しています。ありがとう」と言います。

XXXX には、その人についてや、その人の行ったことを入れます。 自分に与えた影響について説明しても良いです。

2. お互いに感謝を述べる

じっくり考えて発言したい人もいますので、沈黙があっても、待ちます。

ファシリテーターは皆の発言を付箋にメモするのに徹し、 メンバーが発言に集中できるようにしたり、 付箋にメモをして発言していないメンバーを促したりします。

シールや絵文字を用意して「私もそう思う!」付箋に、どんどんマークをしていくのも 感謝の気持ちの表明になります。

3. クロージング

チームメンバー全員に対し「ありがとう」と言い合い、みんなで拍手して終わります。

効果

実際やってみて、メンバーの皆さんから「照れる」「嬉しい」という言葉がありました。 普段伝えているつもりでも、機会を設けて感謝を伝えることで より関係性が良くなり、意見相違があってもお互いに尊重しあえる効果があると言われています。

f:id:takamarix:20200930124420p:plain

ぜひ、やってみてくださいね!

参考リンク

アジャイルレトロスペクティブズ 強いチームを育てる「ふりかえり」の手引き(書籍)

notionでタスク管理した

こんにちは、タンバリン東京開発デザインチームの斎藤です。

今回はタスク管理にnotionを導入した話をしたいと思います。

経緯

タンバリンは今年の春頃から在宅勤務がメインで、僕は出社していた頃と比べてある程度家のことや趣味に時間を費やせるようになりました。

それらのタスクを今までは頭の中で管理していたのですが、どうせなら管理してしまおうと思いたったのが経緯です。

以上を踏まえた上で僕は以下の点を求めました。

  • 日常/趣味/仕事をまとめて管理出来る
  • 上記を大カテゴリとして階層構造でタスクを管理出来る
  • タスクを細かく区切って管理出来る
  • 時間割的な感じでその日にやることがわかる

可能であったら以下の機能もあるといいなと思っていました。

  • 読んだ本の情報や感想もそこにあると嬉しい
  • 仕事の情報や勉強したことがそこにあると嬉しい

逆に以下の点は求めませんでした。

  • オフライン対応(ずっと家にいるから)
  • 表計算(お金周りは別のを使ってる)

これらの要素を満たすのがnotionかなと思ったのでnotionを使うことにしました。

notionの使い方の予定を組む

まず、なんとなく機能を調べて出来そうな形に落とし込むことにしました。

求める機能 使い方の予想
日常/趣味/仕事をまとめて管理出来る テーブルでタグを使いカテゴライズする
タスクを細かく区切って管理出来る テーブル内にページがあるので、その中に入れ子でテーブル作る
時間割的な感じでその日にやることがわかる 上のを参考にカラムで組む

ぱっと調べた限りではいけそうだと感じたので実装していきました。

日常/趣味/仕事をまとめて管理する

まず、タスクの一覧用のページにテーブルをいれました。

思い立ったタスクは分類問わずとりあえずここに格納しています。

タスクが発生した時はここに登録することから始めます。

f:id:otiasotika:20200916135304p:plain

タスクを細かく区切って管理出来る

テーブルになにか登録すると自動で詳細等を書き込めるページが生成されます。

下の方にテーブルや文章を自由に入れられる自由記述欄があるので、僕はテーブルに細かなタスクを書き出し管理しています。

f:id:otiasotika:20200916141448p:plain

しかし、これだけだと

プロジェクトのテーブルをクリック>タスクが書いてあるテーブルを見る>戻る

と、少し面倒な上にタスクを俯瞰して見れないのでこれらをまとめたページを作ることにしました。

タスクがまとまってるページを作る

ここでは上で作った細かなタスクを書き出したテーブルを転載します。

終わったタスクは消すので、プロジェクトのページ以上に今着手していることがわかりやすくなっています。

また、toggle listを使うことである程度分類出来るようにしました。

仕事系はなるべくネストが深くならないようにし、趣味や家事は気の赴くままネストしていきます。

f:id:otiasotika:20200916143217p:plain

時間割的な感じでその日にやることがわかる

まずこんな感じで目次を作ることにしました。

長く運用していくとこの類は氾濫しそうな予感がしたからです。

単位は一週間ごとで、大体月曜日にテンプレートを複製して、タスクを埋めていく感じです。 f:id:otiasotika:20200916144241p:plain

中身はこうなっています。 1日を朝と就業時間と夜に分けて管理していて、その時間が来たらそのtoggle listを開くみたいな運用です。

文字とかタイトルとかの要素をまとめて選択して、一行目の横にもってくると勝手にカラムにしてくれるので、それを使っています。

また、下のwishlistにはその週やるであろうタスクのテーブルのリンクが添付してあって、そこからタスクを卸してきます。

f:id:otiasotika:20200916150512p:plain

開くとこんな感じになっています。

○とか●はトマトのメタファーです。

普段ポモドーロ法を使っているので○が大体25分。●は一つで2セットを意味します。

休憩時間にやることは、このポモドーロ法を使った時に生まれる休憩時間に上から順にやっていきます。

f:id:otiasotika:20200916150325p:plain

使い方

新たなタスクが生まれたらプロジェクトに登録、データベースに転載、時間割のwishlistに入れていけそうな時間に登録。

通常時は時間割を見て自分のタスクを確認。

適宜、プロジェクトを眺めて趣味のタスクを入れたり、データベースを眺めて終わったものを消したりして管理。

という感じで使っています。 一度作ってしまったらなんでもコピペで済んでしまうので意外と楽でいいです。

まとめ

今回は僕のタスクの管理の方法を紹介しました。

僕の要求はデータベースとリンクで解決出来ましたが、notionには色々な使い方があるようです。

皆さんもぜひ使ってみてください。

https://www.notion.so/