HEX
Server: Microsoft-IIS/10.0
System: Windows NT ITPWINWEBSVR22 10.0 build 20348 (Windows Server 2022) AMD64
User: www.conferencesearch.co.uk (0)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: D:/web/matomo.circ/plugins/SitesManager/vue/src/SitesManagement/SitesManagement.vue
<!--
  Matomo - free/libre analytics platform

  @link    https://matomo.org
  @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
-->

<template>
  <div class="SitesManager" ref="root">
    <div class="sites-manager-header">
      <div v-content-intro>
        <h2
          v-show="availableTypes.length"
        >
          <EnrichedHeadline
            :help-url="externalRawLink('https://matomo.org/docs/manage-websites/')"
            :feature-name="translate('SitesManager_WebsitesManagement')"
          >
            {{ headlineText }}
          </EnrichedHeadline>
        </h2>

        <p>
          {{ translate('SitesManager_MainDescription') }}

          <span v-html="$sanitize(mainDescription)"></span>

          <span v-show="hasSuperUserAccess">
            <br/>
            <span v-html="$sanitize(superUserAccessMessage)"></span>
        </span>
        </p>
      </div>
    </div>
    <div>
      <div :class="{ hide_only: !isLoading }">
        <div class="loadingPiwik">
          <img
            src="plugins/Morpheus/images/loading-blue.gif"
            :alt="translate('General_LoadingData')"
          />
          {{ translate('General_LoadingData') }}
        </div>
      </div>
    </div>

    <div>
      <ButtonBar
        :site-is-being-edited="isSiteBeingEdited"
        :has-prev="hasPrev"
        :hasNext="hasNext"
        :offset-start="offsetStart"
        :offset-end="offsetEnd"
        :total-number-of-sites="totalNumberOfSites"
        :is-loading="isLoading"
        :search-term="searchTerm"
        :is-searching="!!activeSearchTerm"
        @update:search-term="searchTerm = $event"
        @add="addNewEntity()"
        @search="searchSites($event)"
        @prev="previousPage()"
        @next="nextPage()"
      />
    </div>

    <MatomoDialog v-model="showAddSiteDialog">
      <div class="ui-confirm add-site-dialog">
        <div>
          <h2>{{ translate('SitesManager_ChooseMeasurableTypeHeadline') }}</h2>

          <div class="center">
            <p>
              <button
                type="button"
                v-for="type in availableTypes"
                :key="type.id"
                :title="type.description"
                class="modal-close btn"
                @click="addSite(type.id);"
                aria-disabled="false"
              >
                <span class="ui-button-text">{{ type.name }}</span>
              </button>
            </p>
          </div>
        </div>
      </div>
    </MatomoDialog>

    <div class="sitesManagerList">
      <p v-if="activeSearchTerm && 0 === sites.length && !isLoading">
        {{ translate('SitesManager_NotFound') }} <strong>{{ activeSearchTerm }}</strong>
      </p>

      <div
        v-for="(site, index) in sites"
        :key="site.idsite"
      >
        <SiteFields
          :site="site"
          :timezone-support-enabled="timezoneSupportEnabled"
          :utc-time="utcTime"
          :global-settings="globalSettings"
          @edit-site="this.isSiteBeingEdited = true"
          @cancel-edit-site="afterCancelEdit($event)"
          @delete="afterDelete($event)"
          @save="afterSave($event.site, $event.settingValues, index, $event.isNew)"
        />
      </div>
    </div>

    <div class="bottomButtonBar">
      <ButtonBar
        :site-is-being-edited="isSiteBeingEdited"
        :has-prev="hasPrev"
        :hasNext="hasNext"
        :offset-start="offsetStart"
        :offset-end="offsetEnd"
        :total-number-of-sites="totalNumberOfSites"
        :is-loading="isLoading"
        :search-term="searchTerm"
        :is-searching="!!activeSearchTerm"
        @update:search-term="searchTerm = $event"
        @add="addNewEntity()"
        @search="searchSites($event)"
        @prev="previousPage()"
        @next="nextPage()"
      />
    </div>
  </div>

</template>

<script lang="ts">
import { defineComponent, watch } from 'vue';
import {
  Matomo,
  MatomoDialog,
  Site,
  ContentIntro,
  EnrichedHeadline,
  AjaxHelper,
  MatomoUrl,
  translate,
} from 'CoreHome';
import { Setting } from 'CorePluginsAdmin';
import ButtonBar from './ButtonBar.vue';
import SiteFields from '../SiteFields/SiteFields.vue';
import SiteTypesStore from '../SiteTypesStore/SiteTypesStore';
import TimezoneStore from '../TimezoneStore/TimezoneStore';
import GlobalSettingsStore from '../GlobalSettingsStore/GlobalSettingsStore';

interface SitesManagementState {
  pageSize: number;
  currentPage: number;
  showAddSiteDialog: boolean;
  searchTerm: string;
  activeSearchTerm: string;
  fetchedSites: Site[];
  utcTime: Date;
  totalNumberOfSites: number|null;
  isLoadingInitialEntities: boolean;
  isSiteBeingEdited: boolean;
  fetchLimitedSitesAbortController: null|AbortController,
}

export default defineComponent({
  props: {
    // TypeScript can't add state types if there are no properties (probably a bug in Vue)
    // so we add one dummy property to get the compile to work
    dummy: String,
  },
  components: {
    MatomoDialog,
    ButtonBar,
    SiteFields,
    EnrichedHeadline,
  },
  directives: {
    ContentIntro,
  },
  data(): SitesManagementState {
    const currentDate = new Date();
    const utcTime = new Date(
      currentDate.getUTCFullYear(),
      currentDate.getUTCMonth(),
      currentDate.getUTCDate(),
      currentDate.getUTCHours(),
      currentDate.getUTCMinutes(),
      currentDate.getUTCSeconds(),
    );

    return {
      pageSize: 10,
      currentPage: 0,
      showAddSiteDialog: false,
      searchTerm: '',
      activeSearchTerm: '',
      fetchedSites: [],
      isLoadingInitialEntities: false,
      utcTime,
      totalNumberOfSites: null,
      isSiteBeingEdited: false,
      fetchLimitedSitesAbortController: null,
    };
  },
  created() {
    TimezoneStore.init();
    SiteTypesStore.init();
    GlobalSettingsStore.init();

    this.isLoadingInitialEntities = true;
    Promise.all([
      SiteTypesStore.fetchAvailableTypes(),
      this.fetchLimitedSitesWithAdminAccess(),
      this.getTotalNumberOfSites(),
    ]).then(() => {
      this.triggerAddSiteIfRequested();
    }).finally(() => {
      this.isLoadingInitialEntities = false;
    });

    // if hash is #globalSettings, redirect to globalSettings action (we don't do it on
    // page load so the back button still works)
    watch(() => MatomoUrl.hashQuery.value, () => {
      this.checkGlobalSettingsHash();
    });
  },
  computed: {
    sites() {
      const emptyIdSiteRows = this.fetchedSites.filter((s) => !s.idsite).length;
      return this.fetchedSites.slice(0, this.pageSize + emptyIdSiteRows);
    },
    isLoading() {
      return !!this.fetchLimitedSitesAbortController
        || this.isLoadingInitialEntities
        || this.totalNumberOfSites === null
        || SiteTypesStore.isLoading.value
        || TimezoneStore.isLoading.value
        || GlobalSettingsStore.isLoading.value;
    },
    availableTypes() {
      return SiteTypesStore.types.value;
    },
    timezoneSupportEnabled() {
      return TimezoneStore.timezoneSupportEnabled.value;
    },
    globalSettings() {
      return GlobalSettingsStore.globalSettings.value;
    },
    headlineText() {
      return translate(
        'SitesManager_XManagement',
        this.availableTypes.length > 1
          ? translate('General_Measurables')
          : translate('SitesManager_Sites'),
      );
    },
    mainDescription() {
      return translate(
        'SitesManager_YouCurrentlyHaveAccessToNWebsites',
        `<strong>${this.totalNumberOfSites}</strong>`,
      );
    },
    hasSuperUserAccess() {
      return Matomo.hasSuperUserAccess;
    },
    superUserAccessMessage() {
      return translate('SitesManager_SuperUserAccessCan', '<a href=\'#globalSettings\'>', '</a>');
    },
    hasPrev() {
      return this.currentPage >= 1;
    },
    hasNext() {
      return this.fetchedSites.filter((s) => !!s.idsite).length >= this.pageSize + 1;
    },
    offsetStart() {
      return this.currentPage * this.pageSize + 1;
    },
    offsetEnd() {
      return this.offsetStart + this.sites.filter((s) => !!s.idsite).length - 1;
    },
  },
  methods: {
    checkGlobalSettingsHash() {
      const newHash = MatomoUrl.hashQuery.value;
      if (Matomo.hasSuperUserAccess
        && (
          newHash === 'globalSettings'
          || newHash === '/globalSettings'
        )
      ) {
        MatomoUrl.updateLocation({
          ...MatomoUrl.urlParsed.value,
          action: 'globalSettings',
        });
      }
    },
    addNewEntity() {
      if (this.availableTypes.length > 1) {
        this.showAddSiteDialog = true;
      } else if (this.availableTypes.length === 1) {
        this.addSite(this.availableTypes[0].id);
      }
    },
    addSite(typeId?: string) {
      let type: string|undefined = typeId;

      const parameters = {
        isAllowed: true,
        measurableType: type,
      };

      Matomo.postEvent('SitesManager.initAddSite', parameters);

      if (parameters && !parameters.isAllowed) {
        return;
      }

      if (!type) {
        type = 'website'; // todo shall we really hard code this or trigger an exception or so?
      }

      this.fetchedSites.unshift({
        type,
      } as unknown as Site);

      this.isSiteBeingEdited = true;
    },
    afterCancelEdit({ site, element }: { site: Site, element: HTMLElement }) {
      this.isSiteBeingEdited = false;

      if (!site.idsite) {
        this.fetchedSites = this.fetchedSites.filter((s) => !!s.idsite);
        return;
      }

      element.scrollIntoView();
    },
    fetchLimitedSitesWithAdminAccess(searchTerm = '') {
      if (this.fetchLimitedSitesAbortController) {
        this.fetchLimitedSitesAbortController.abort();
      }

      this.fetchLimitedSitesAbortController = new AbortController();

      const limit = this.pageSize + 1;
      const offset = this.currentPage * this.pageSize;

      const params: QueryParameters = {
        method: 'SitesManager.getSitesWithAdminAccess',
        fetchAliasUrls: 1,
        limit: limit + offset, // this is applied in SitesManager.getSitesWithAdminAccess API
        filter_offset: offset, // filter_offset and filter_limit is applied in response builder
        filter_limit: limit,
      };

      if (searchTerm) {
        params.pattern = searchTerm;
      }

      return AjaxHelper.fetch<Site[]>(params).then((sites) => {
        this.fetchedSites = sites || [];
      }).then((sites) => {
        this.activeSearchTerm = searchTerm;
        return sites;
      }).finally(() => {
        this.fetchLimitedSitesAbortController = null;
      });
    },
    getTotalNumberOfSites() {
      return AjaxHelper.fetch<string[]>({
        method: 'SitesManager.getSitesIdWithAdminAccess',
        filter_limit: '-1',
      }).then((sites) => {
        this.totalNumberOfSites = sites.length;
      });
    },
    triggerAddSiteIfRequested() {
      const forcedEditSiteId = SiteTypesStore.getEditSiteIdParameter();
      const showaddsite = MatomoUrl.urlParsed.value.showaddsite as string;

      if (showaddsite === '1') {
        this.addNewEntity();
      } else if (forcedEditSiteId) {
        this.searchTerm = forcedEditSiteId;
        this.fetchLimitedSitesWithAdminAccess(this.searchTerm);
      }
    },
    previousPage() {
      this.currentPage = Math.max(0, this.currentPage - 1);
      this.fetchLimitedSitesWithAdminAccess(this.activeSearchTerm);
    },
    nextPage() {
      this.currentPage = Math.max(0, this.currentPage + 1);
      this.fetchLimitedSitesWithAdminAccess(this.activeSearchTerm);
    },
    searchSites() {
      this.currentPage = 0;
      this.fetchLimitedSitesWithAdminAccess(this.searchTerm);
    },
    afterDelete(site: Site) {
      let redirectParams: QueryParameters = {
        showaddsite: 0,
      };

      // if the current idSite in the URL is the site we're deleting, then we have to make to
      // change it. otherwise, if a user goes to another page, the invalid idSite may cause
      // a fatal error.
      if (MatomoUrl.urlParsed.value.idSite === `${site.idsite}`) {
        const otherSite = this.sites.find((s) => s.idsite !== site.idsite);

        if (otherSite) {
          redirectParams = { ...redirectParams, idSite: otherSite.idsite };
        }
      }

      Matomo.helper.redirect(redirectParams);
    },
    afterSave(site: Site, settingValues: Record<string, Setting[]>, index: number, isNew: boolean) {
      const texttareaArrayParams = [
        'excluded_ips',
        'excluded_parameters',
        'excluded_user_agents',
        'sitesearch_keyword_parameters',
        'sitesearch_category_parameters',
      ];

      const newSite: Site = { ...site };

      Object.values(settingValues).forEach((settings) => {
        settings.forEach((setting) => {
          if (setting.name === 'urls') {
            newSite.alias_urls = setting.value as string[];
          } else if (texttareaArrayParams.indexOf(setting.name) !== -1) {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            (newSite as any)[setting.name] = (setting.value as string[]).join(', ');
          } else {
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            (newSite as any)[setting.name] = setting.value;
          }
        });
      });

      this.fetchedSites[index] = newSite;

      if (isNew && this.totalNumberOfSites !== null) {
        this.totalNumberOfSites += 1;
      }

      this.isSiteBeingEdited = false;
    },
  },
});
</script>